From edb6171d19fbc698434eef37bfec755a27b57b75 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Thu, 9 Apr 2026 23:44:13 +0200 Subject: [PATCH 01/29] refactor repo --- README.md | 32 ++ codemods/error-boundaries/README.md | 16 + .../error-boundaries}/codemod.yaml | 6 +- .../error-boundaries}/package.json | 5 +- .../error-boundaries}/scripts/codemod.ts | 8 +- .../tests/class-component}/expected.tsx | 0 .../tests/class-component}/input.tsx | 0 .../tests/class-component}/metrics.json | 2 +- .../create-class-component}/expected.tsx | 0 .../tests/create-class-component}/input.tsx | 0 .../create-class-component}/metrics.json | 2 +- .../error-boundaries}/tsconfig.json | 16 +- .../error-boundaries}/workflow.yaml | 0 codemods/find-dom-node/README.md | 16 + .../find-dom-node}/codemod.yaml | 6 +- .../find-dom-node}/package.json | 5 +- .../find-dom-node}/scripts/codemod.ts | 8 +- .../tests/findDOMNode/expected.tsx | 34 ++ .../find-dom-node/tests/findDOMNode/input.tsx | 34 ++ .../tests/findDOMNode}/metrics.json | 4 +- .../find-dom-node}/tsconfig.json | 16 +- .../find-dom-node}/workflow.yaml | 0 codemods/manual-bind-to-arrow/README.md | 16 + .../manual-bind-to-arrow}/codemod.yaml | 6 +- .../manual-bind-to-arrow}/package.json | 5 +- .../manual-bind-to-arrow}/scripts/codemod.ts | 100 ++++-- .../tests/manual-bind-to-arrow1}/expected.tsx | 2 +- .../tests/manual-bind-to-arrow1}/input.tsx | 0 .../tests/manual-bind-to-arrow1}/metrics.json | 2 +- .../tests/manual-bind-to-arrow10/expected.tsx | 7 + .../tests/manual-bind-to-arrow10/input.tsx | 15 + .../manual-bind-to-arrow10}/metrics.json | 2 +- .../tests/manual-bind-to-arrow11/expected.tsx | 12 + .../tests/manual-bind-to-arrow11/input.tsx | 12 + .../tests/manual-bind-to-arrow2/expected.tsx | 18 + .../tests/manual-bind-to-arrow2/input.tsx | 19 + .../tests/manual-bind-to-arrow2}/metrics.json | 2 +- .../tests/manual-bind-to-arrow3}/expected.tsx | 0 .../tests/manual-bind-to-arrow3}/input.tsx | 0 .../tests/manual-bind-to-arrow4/expected.tsx | 3 + .../tests/manual-bind-to-arrow4/input.tsx | 7 + .../tests/manual-bind-to-arrow4}/metrics.json | 2 +- .../tests/manual-bind-to-arrow5/expected.tsx | 3 + .../tests/manual-bind-to-arrow5/input.tsx | 8 + .../tests/manual-bind-to-arrow5/metrics.json | 10 + .../tests/manual-bind-to-arrow6/expected.tsx | 9 + .../tests/manual-bind-to-arrow6/input.tsx | 10 + .../tests/manual-bind-to-arrow6/metrics.json | 10 + .../tests/manual-bind-to-arrow7/expected.tsx | 9 + .../tests/manual-bind-to-arrow7/input.tsx | 10 + .../tests/manual-bind-to-arrow7/metrics.json | 10 + .../tests/manual-bind-to-arrow8/expected.tsx | 3 + .../tests/manual-bind-to-arrow8/input.tsx | 7 + .../tests/manual-bind-to-arrow8/metrics.json | 10 + .../tests/manual-bind-to-arrow9/expected.tsx | 7 + .../tests/manual-bind-to-arrow9/input.tsx | 15 + .../tests/manual-bind-to-arrow9}/metrics.json | 2 +- codemods/manual-bind-to-arrow/tsconfig.json | 22 ++ .../manual-bind-to-arrow}/workflow.yaml | 0 codemods/pure-render-mixin/README.md | 20 ++ .../pure-render-mixin}/codemod.yaml | 6 +- .../pure-render-mixin}/package.json | 5 +- .../pure-render-mixin}/scripts/codemod.ts | 29 +- .../tests/pure-render-mixin}/expected.tsx | 11 +- .../tests/pure-render-mixin}/input.tsx | 0 .../tests/pure-render-mixin}/metrics.json | 2 +- .../tests/pure-render-mixin2}/expected.tsx | 0 .../tests/pure-render-mixin2}/input.tsx | 0 .../tests/pure-render-mixin2}/metrics.json | 2 +- .../tests/pure-render-mixin3}/expected.tsx | 0 .../tests/pure-render-mixin3}/input.tsx | 0 .../tests/pure-render-mixin3}/metrics.json | 2 +- .../tests/pure-render-mixin4}/expected.tsx | 10 +- .../tests/pure-render-mixin4/input.tsx | 12 + .../tests/pure-render-mixin4}/metrics.json | 2 +- .../tests/pure-render-mixin4/test.config.json | 5 + .../pure-render-mixin}/tsconfig.json | 16 +- .../pure-render-mixin}/workflow.yaml | 8 + .../react-proptypes-to-prop-types/README.md | 20 ++ .../codemod.yaml | 6 +- .../package.json | 5 +- .../scripts/codemod.ts | 337 ++++++++++++++++++ .../already-migrated-import/expected.tsx | 5 + .../tests/already-migrated-import/input.tsx | 5 + .../already-migrated-import}/metrics.json | 2 +- .../expected.tsx | 5 + .../input.tsx | 5 + .../metrics.json | 10 + .../expected.tsx | 5 + .../already-migrated-named-import/input.tsx | 5 + .../metrics.json | 10 + .../already-migrated-require/expected.tsx | 5 + .../tests/already-migrated-require/input.tsx | 5 + .../already-migrated-require/metrics.json | 10 + .../assigned-from-react-var/expected.tsx | 5 + .../tests/assigned-from-react-var/input.tsx | 9 + .../assigned-from-react-var/metrics.json | 10 + .../expected.tsx | 6 + .../input.tsx | 7 + .../metrics.json | 10 + .../default-and-named-import/expected.tsx | 18 + .../tests/default-and-named-import/input.tsx | 17 + .../tests/default-import/expected.tsx | 22 ++ .../tests/default-import/input.tsx | 21 ++ .../tests/default-import}/metrics.json | 2 +- .../expected.tsx | 5 + .../destructured-proptypes-import/input.tsx | 5 + .../tests/import-alias/expected.tsx | 8 + .../tests/import-alias/input.tsx | 7 + .../expected.tsx | 4 + .../import-flow-type-with-require/input.tsx | 5 + .../mixed-import-and-require-2/expected.tsx | 14 + .../mixed-import-and-require-2/input.tsx | 15 + .../mixed-import-and-require/expected.tsx | 10 + .../tests/mixed-import-and-require/input.tsx | 9 + .../mixed-import-and-require/metrics.json | 10 + .../tests/module-name/expected.tsx | 18 + .../tests/module-name/input.tsx | 17 + .../tests/module-name}/metrics.json | 4 +- .../tests/module-name/test.config.json | 5 + .../tests/named-parameters/expected.tsx | 21 ++ .../tests/named-parameters/input.tsx | 20 ++ .../expected.tsx | 7 + .../input.tsx | 11 + .../tests/no-change-import/expected.tsx | 18 + .../tests/no-change-import/input.tsx | 18 + .../tests/no-change-require/expected.tsx | 18 + .../tests/no-change-require/input.tsx | 18 + .../tests/require-alias/expected.tsx | 9 + .../tests/require-alias/input.tsx | 8 + .../require-destructured-direct/expected.tsx | 8 + .../require-destructured-direct/input.tsx | 9 + .../require-destructured-multi/expected.tsx | 19 + .../require-destructured-multi/input.tsx | 18 + .../require-destructured-only/expected.tsx | 20 ++ .../tests/require-destructured-only/input.tsx | 20 ++ .../tests/require/expected.tsx | 18 + .../tests/require/input.tsx | 17 + .../tests/require/metrics.json | 10 + .../tests/with-top-comment/expected.tsx | 12 + .../tests/with-top-comment/input.tsx | 11 + .../tsconfig.json | 16 +- .../workflow.yaml | 8 + .../rename-unsafe-lifecycles}/.gitignore | 0 codemods/rename-unsafe-lifecycles/README.md | 16 + .../rename-unsafe-lifecycles}/codemod.yaml | 6 +- .../rename-unsafe-lifecycles}/package.json | 5 +- .../rename-unsafe-lifecycles}/pnpm-lock.yaml | 0 .../scripts/codemod.ts | 8 +- .../tests/arrow-functions}/expected.tsx | 2 +- .../tests/arrow-functions}/input.tsx | 2 +- .../tests/arrow-functions}/metrics.json | 2 +- .../tests/create-react-class}/expected.tsx | 2 +- .../tests/create-react-class}/input.tsx | 2 +- .../tests/create-react-class}/metrics.json | 2 +- .../tests/instance-methods}/expected.tsx | 2 +- .../tests/instance-methods}/input.tsx | 2 +- .../tests/instance-methods}/metrics.json | 2 +- .../manually-calling-lifecycles/expected.tsx | 3 + .../manually-calling-lifecycles/input.tsx | 3 + .../manually-calling-lifecycles}/metrics.json | 2 +- .../expected.tsx | 20 ++ .../manually-invoked-mixin-methods/input.tsx | 20 ++ .../metrics.json | 2 +- .../one-lifecycle-calls-another/expected.tsx | 11 + .../one-lifecycle-calls-another/input.tsx | 11 + .../one-lifecycle-calls-another}/metrics.json | 4 +- .../tests/standalone-function/expected.tsx | 9 + .../tests/standalone-function/input.tsx | 9 + .../tests/typescript-class-tsx/expected.tsx | 14 + .../tests/typescript-class-tsx/input.tsx | 14 + .../tests/typescript-class-tsx}/metrics.json | 2 +- .../variable-within-class-method/expected.tsx | 7 + .../variable-within-class-method/input.tsx | 7 + .../rename-unsafe-lifecycles/tsconfig.json | 27 ++ .../rename-unsafe-lifecycles}/workflow.yaml | 0 codemods/replace-act-import/README.md | 16 + .../replace-act-import}/codemod.yaml | 6 +- .../replace-act-import}/package.json | 5 +- .../replace-act-import}/scripts/codemod.ts | 67 +++- .../tests/default-import/expected.tsx | 0 .../tests/default-import/input.tsx | 0 .../tests/default-import/metrics.json | 0 .../tests/named-import-2/expected.tsx | 1 + .../tests/named-import-2/input.tsx | 0 .../tests/named-import-2/metrics.json | 0 .../tests/named-import/expected.tsx | 2 + .../tests/named-import/input.tsx | 0 .../tests/named-import/metrics.json | 0 .../tests/other-import/expected.tsx | 0 .../tests/other-import/input.tsx | 0 .../tests/re-export/expected.tsx | 0 .../tests/re-export/input.tsx | 0 .../tests/re-export/metrics.json | 0 .../tests/wildcard-import-2/expected.tsx | 3 + .../tests/wildcard-import-2/input.tsx | 0 .../tests/wildcard-import-2/metrics.json | 0 .../tests/wildcard-import/expected.tsx | 2 + .../tests/wildcard-import/input.tsx | 0 .../tests/wildcard-import/metrics.json | 0 codemods/replace-act-import/tsconfig.json | 27 ++ .../replace-act-import}/workflow.yaml | 0 .../replace-reactdom-render}/.gitignore | 0 codemods/replace-reactdom-render/README.md | 16 + .../replace-reactdom-render}/codemod.yaml | 6 +- .../replace-reactdom-render}/package.json | 5 +- .../replace-reactdom-render}/pnpm-lock.yaml | 0 .../scripts/codemod.ts | 36 +- .../tests/default}/expected.tsx | 4 +- .../tests/default}/input.tsx | 0 .../tests/default}/metrics.json | 2 +- .../tests/nested/expected.tsx | 9 + .../tests/nested}/input.tsx | 0 .../tests/nested}/metrics.json | 2 +- .../replace-reactdom-render/tsconfig.json | 27 ++ .../replace-reactdom-render}/workflow.yaml | 0 .../replace-string-ref}/.gitignore | 0 codemods/replace-string-ref/README.md | 16 + .../replace-string-ref}/codemod.yaml | 6 +- .../replace-string-ref}/package.json | 5 +- .../replace-string-ref}/pnpm-lock.yaml | 0 .../replace-string-ref}/scripts/codemod.ts | 31 +- .../expected.tsx | 8 +- .../input.tsx | 0 .../metrics.json | 2 +- .../expected.tsx | 8 +- .../class-component-default-import}/input.tsx | 0 .../metrics.json | 2 +- .../expected.tsx | 8 +- .../class-component-named-import}/input.tsx | 0 .../metrics.json | 2 +- .../tests/function-component}/expected.tsx | 0 .../tests/function-component}/input.tsx | 0 codemods/replace-string-ref/tsconfig.json | 27 ++ .../replace-string-ref}/workflow.yaml | 0 codemods/replace-use-form-state/README.md | 16 + .../replace-use-form-state}/codemod.yaml | 6 +- .../replace-use-form-state}/package.json | 5 +- .../scripts/codemod.ts | 12 +- .../tests/default-import/expected.tsx | 0 .../tests/default-import/input.tsx | 0 .../tests/default-import/metrics.json | 0 .../tests/named-import-2/expected.tsx | 0 .../tests/named-import-2/input.tsx | 0 .../tests/named-import-2/metrics.json | 0 .../tests/named-import-3/expected.tsx | 0 .../tests/named-import-3/input.tsx | 0 .../tests/named-import-3/metrics.json | 0 .../tests/named-import/expected.tsx | 0 .../tests/named-import/input.tsx | 0 .../tests/named-import/metrics.json | 0 .../tests/other-import/expected.tsx | 0 .../tests/other-import/input.tsx | 0 .../tests/wildcard-import/expected.tsx | 0 .../tests/wildcard-import/input.tsx | 0 .../tests/wildcard-import/metrics.json | 0 codemods/replace-use-form-state/tsconfig.json | 27 ++ .../replace-use-form-state}/workflow.yaml | 0 codemods/sort-comp/README.md | 20 ++ .../sort-comp}/codemod.yaml | 6 +- .../sort-comp}/package.json | 5 +- codemods/sort-comp/scripts/codemod.ts | 200 +++++++++++ .../custom-sort-custom-sort/expected.tsx | 21 ++ .../tests/custom-sort-custom-sort/input.tsx | 21 ++ .../custom-sort-custom-sort/metrics.json | 11 + .../custom-sort-custom-sort/test.config.json | 10 + .../expected.tsx | 24 ++ .../input.tsx | 24 ++ .../metrics.json | 11 + .../test.config.json | 11 + .../tests/sort-comp-pure/expected.tsx | 43 +++ .../sort-comp/tests/sort-comp-pure/input.tsx | 43 +++ .../tests/sort-comp-pure}/metrics.json | 3 +- .../sort-comp/tests/sort-comp}/expected.tsx | 7 +- .../sort-comp/tests/sort-comp}/input.tsx | 0 .../sort-comp/tests/sort-comp}/metrics.json | 3 +- .../sort-comp/tests/sort-comp2/expected.tsx | 43 +++ codemods/sort-comp/tests/sort-comp2/input.tsx | 43 +++ .../sort-comp/tests/sort-comp2}/metrics.json | 3 +- .../sort-comp/tests/sort-comp3/expected.tsx | 43 +++ codemods/sort-comp/tests/sort-comp3/input.tsx | 43 +++ .../sort-comp/tests/sort-comp3}/metrics.json | 3 +- .../sort-comp}/tsconfig.json | 16 +- .../sort-comp}/workflow.yaml | 9 + .../update-react-imports}/.gitignore | 0 codemods/update-react-imports/README.md | 20 ++ .../update-react-imports}/codemod.yaml | 6 +- .../update-react-imports}/package.json | 5 +- .../update-react-imports}/pnpm-lock.yaml | 0 .../update-react-imports/scripts/codemod.ts | 250 +++++++++++++ .../expected.tsx | 5 + .../input.tsx | 5 + .../metrics.json | 12 + .../expected.tsx | 3 + .../input.tsx | 3 + .../metrics.json | 12 + .../expected.tsx | 5 +- .../input.tsx | 0 .../metrics.json | 12 + .../test.config.json | 5 + .../expected.tsx | 6 + .../input.tsx | 6 + .../metrics.json | 12 + .../test.config.json | 5 + .../destructure-named-imports/expected.tsx | 4 + .../tests/destructure-named-imports/input.tsx | 5 + .../destructure-named-imports}/metrics.json | 4 +- .../test.config.json | 5 + .../expected.tsx | 5 + .../input.tsx | 5 + .../metrics.json | 12 + .../expected.tsx | 2 + .../input.tsx | 3 + .../metrics.json | 12 + .../tests/jsx-element/expected.tsx | 1 + .../tests/jsx-element/input.tsx | 3 + .../tests/jsx-element/metrics.json | 12 + .../tests/jsx-fragment/expected.tsx | 1 + .../tests/jsx-fragment/input.tsx | 3 + .../tests/jsx-fragment/metrics.json | 12 + .../tests/leading-comment/expected.tsx | 5 + .../tests/leading-comment/input.tsx | 7 + .../tests/leading-comment/metrics.json | 12 + .../expected.tsx | 2 + .../react-already-used-named-export/input.tsx | 3 + .../metrics.json | 12 + .../expected.tsx | 5 +- .../input.tsx | 4 +- .../metrics.json | 12 + .../expected.tsx | 1 + .../input.tsx | 3 +- .../metrics.json | 12 + .../react-basic-default-export}/expected.tsx | 1 - .../react-basic-default-export}/input.tsx | 0 .../react-basic-default-export}/metrics.json | 4 +- .../react-jsx-member-expression/expected.tsx | 2 + .../react-jsx-member-expression/input.tsx | 3 + .../react-jsx-member-expression}/metrics.json | 4 +- .../tests/react-not-removed/expected.tsx | 6 + .../tests/react-not-removed}/input.tsx | 2 +- .../tests/react-not-removed}/metrics.json | 4 +- .../react-type-default-export/expected.tsx | 2 + .../tests/react-type-default-export/input.tsx | 4 + .../react-type-default-export/metrics.json | 12 + .../tests/react-type-not-removed/expected.tsx | 2 + .../tests/react-type-not-removed/input.tsx | 4 + .../tests/react-type-not-removed/metrics.json | 12 + .../expected.tsx | 5 + .../input.tsx | 5 + .../metrics.json | 12 + .../expected.tsx | 2 + .../input.tsx | 3 + .../metrics.json | 12 + .../typescript-jsx-element-tsx/expected.tsx | 1 + .../typescript-jsx-element-tsx/input.tsx | 3 + .../typescript-jsx-element-tsx/metrics.json | 12 + .../typescript-jsx-fragment-tsx/expected.tsx | 1 + .../typescript-jsx-fragment-tsx/input.tsx | 3 + .../typescript-jsx-fragment-tsx/metrics.json | 12 + .../expected.tsx | 5 + .../typescript-leading-comment-tsx/input.tsx | 7 + .../metrics.json | 12 + .../expected.tsx | 4 + .../input.tsx | 5 + .../metrics.json | 12 + .../expected.tsx | 4 + .../input.tsx | 5 + .../metrics.json | 12 + .../expected.tsx | 2 + .../input.tsx | 3 + .../metrics.json | 12 + .../expected.tsx | 4 + .../input.tsx | 5 + .../metrics.json | 12 + .../expected.tsx | 1 + .../input.tsx | 3 + .../metrics.json | 12 + .../expected.tsx | 2 + .../input.tsx | 3 + .../metrics.json | 6 +- .../expected.tsx | 2 + .../input.tsx | 3 + .../metrics.json | 12 + .../expected.tsx | 6 + .../input.tsx | 4 +- .../metrics.json | 12 + .../expected.tsx | 6 + .../input.tsx | 7 + .../metrics.json | 12 + .../tests/variable-already-used/expected.tsx | 6 + .../tests/variable-already-used/input.tsx | 7 + .../tests/variable-already-used/metrics.json | 12 + codemods/update-react-imports/tsconfig.json | 27 ++ .../update-react-imports}/workflow.yaml | 8 + .../use-context-hook}/.gitignore | 0 codemods/use-context-hook/README.md | 16 + .../use-context-hook}/codemod.yaml | 6 +- .../use-context-hook}/package.json | 5 +- .../use-context-hook}/pnpm-lock.yaml | 0 .../use-context-hook}/scripts/codemod.ts | 12 +- .../tests/any-use-context/expected.tsx | 1 + .../tests/any-use-context/input.tsx | 1 + .../tests/mixed-import}/expected.tsx | 2 +- .../tests/mixed-import}/input.tsx | 2 +- .../tests/mixed-import/metrics.json | 0 .../typescript-any-use-context/expected.tsx | 8 + .../typescript-any-use-context/input.tsx | 8 + .../typescript-use-context-2}/expected.tsx | 12 +- .../tests/typescript-use-context-2}/input.tsx | 12 +- .../typescript-use-context-2}/metrics.json | 4 +- .../typescript-use-context}/expected.tsx | 2 +- .../tests/typescript-use-context}/input.tsx | 2 +- .../typescript-use-context}/metrics.json | 2 +- .../tests/use-context-2}/expected.tsx | 2 +- .../tests/use-context-2}/input.tsx | 2 +- .../tests/use-context-2}/metrics.json | 2 +- .../tests/use-context}/expected.tsx | 2 +- .../tests/use-context}/input.tsx | 2 +- .../tests/use-context}/metrics.json | 2 +- codemods/use-context-hook/tsconfig.json | 27 ++ .../use-context-hook}/workflow.yaml | 0 .../tests/arrow-assignment/expected.tsx | 11 - .../tests/arrow-assignment/input.tsx | 11 - .../tests/arrow-assignment/metrics.json | 10 - .../tests/export-default-class/expected.tsx | 13 - .../tests/export-default-class/input.tsx | 13 - .../tests/export-default-class/metrics.json | 10 - .../tests/mixed-methods/expected.tsx | 26 -- .../tests/mixed-methods/input.tsx | 26 -- .../tests/mixed-methods/metrics.json | 10 - .../no-unstable-handleError/expected.tsx | 11 - .../tests/no-unstable-handleError/input.tsx | 11 - .../tests/object-literal-method/expected.tsx | 8 - .../tests/object-literal-method/input.tsx | 8 - .../expected.tsx | 19 - .../input.tsx | 19 - .../metrics.json | 10 - .../tests/similar-name-no-op/expected.tsx | 10 - .../tests/similar-name-no-op/input.tsx | 10 - .../tests/single-method/expected.tsx | 12 - .../tests/single-method/input.tsx | 12 - .../tests/single-method/metrics.json | 10 - .../tests/three-classes/expected.tsx | 22 -- .../tests/three-classes/input.tsx | 22 -- .../tests/three-classes/metrics.json | 10 - .../tests/typescript-method/expected.tsx | 15 - .../tests/typescript-method/input.tsx | 15 - .../tests/typescript-method/metrics.json | 10 - .../comment-and-string-unchanged/expected.tsx | 17 - .../comment-and-string-unchanged/input.tsx | 17 - .../tests/enzyme-wrapper-style/expected.tsx | 17 - .../tests/enzyme-wrapper-style/input.tsx | 17 - .../tests/enzyme-wrapper-style/metrics.json | 10 - .../tests/inside-another-call/expected.tsx | 24 -- .../tests/inside-another-call/input.tsx | 24 -- .../tests/inside-another-call/metrics.json | 10 - .../tests/multiple-calls/expected.tsx | 13 - find-dom-node/tests/multiple-calls/input.tsx | 13 - .../tests/multiple-calls/metrics.json | 10 - find-dom-node/tests/nested-refs/expected.tsx | 28 -- find-dom-node/tests/nested-refs/input.tsx | 28 -- .../tests/no-getDOMNode/expected.tsx | 10 - find-dom-node/tests/no-getDOMNode/input.tsx | 10 - .../tests/react-import-alias/expected.tsx | 14 - .../tests/react-import-alias/input.tsx | 14 - .../tests/react-import-alias/metrics.json | 10 - .../real-world-autosize-input/expected.tsx | 25 -- .../tests/real-world-autosize-input/input.tsx | 25 -- .../real-world-autosize-input/metrics.json | 10 - .../tests/refs-getDOMNode/expected.tsx | 15 - find-dom-node/tests/refs-getDOMNode/input.tsx | 15 - .../tests/refs-getDOMNode/metrics.json | 10 - .../tests/this-getDOMNode/expected.tsx | 11 - find-dom-node/tests/this-getDOMNode/input.tsx | 11 - .../tests/this-getDOMNode/metrics.json | 10 - .../two-components-same-file/expected.tsx | 29 -- .../tests/two-components-same-file/input.tsx | 29 -- .../two-components-same-file/metrics.json | 10 - .../tests/variable-holding-ref/expected.tsx | 24 -- .../tests/variable-holding-ref/input.tsx | 24 -- .../tests/variable-holding-ref/metrics.json | 10 - .../expected.tsx | 13 - .../bind-outside-constructor-no-op/input.tsx | 13 - .../expected.tsx | 14 - .../input.tsx | 15 - .../tests/constructor-with-props/expected.tsx | 13 - .../tests/constructor-with-props/input.tsx | 18 - .../tests/no-bind/expected.tsx | 6 - manual-bind-to-arrow/tests/no-bind/input.tsx | 6 - .../tests/real-world-dropdown/expected.tsx | 28 -- .../tests/real-world-dropdown/input.tsx | 34 -- .../real-world-form-two-binds/expected.tsx | 20 -- .../tests/real-world-form-two-binds/input.tsx | 26 -- .../real-world-form-two-binds/metrics.json | 10 - .../tests/real-world-todo-item/expected.tsx | 20 -- .../tests/real-world-todo-item/input.tsx | 26 -- manual-bind-to-arrow/tsconfig.json | 12 - package.json | 29 +- pnpm-lock.yaml | 145 +++++--- pnpm-workspace.yaml | 14 +- .../tests/create-class-direct-call/input.tsx | 13 - .../create-class-direct-call/metrics.json | 10 - .../tests/no-pure-render-mixin/expected.tsx | 11 - .../tests/no-pure-render-mixin/input.tsx | 11 - .../metrics.json | 10 - .../tests/real-world-form/expected.tsx | 29 -- .../tests/real-world-form/input.tsx | 26 -- .../tests/real-world-list-item/expected.tsx | 33 -- .../tests/real-world-list-item/input.tsx | 30 -- .../expected.tsx | 37 -- .../input.tsx | 31 -- .../metrics.json | 10 - .../expected.tsx | 21 -- .../two-components-one-with-mixin/input.tsx | 18 - .../metrics.json | 10 - .../scripts/codemod.ts | 53 --- .../tests/array-and-object/expected.tsx | 12 - .../tests/array-and-object/input.tsx | 11 - .../tests/basic-prop-types/expected.tsx | 12 - .../tests/basic-prop-types/input.tsx | 12 - .../tests/multiple-types/expected.tsx | 13 - .../tests/multiple-types/input.tsx | 12 - .../tests/no-react-proptypes/expected.tsx | 5 - .../tests/no-react-proptypes/input.tsx | 5 - rename-unsafe-lifecycles/README.md | 38 -- .../tests/arrow-functions/expected.tsx | 13 - .../tests/arrow-functions/input.tsx | 13 - .../tests/class-methods/expected.tsx | 25 -- .../tests/class-methods/input.tsx | 25 -- .../componentWillUpdate-only/expected.tsx | 13 - .../tests/componentWillUpdate-only/input.tsx | 13 - .../componentWillUpdate-only/metrics.json | 11 - .../tests/member-expression-call/expected.tsx | 17 - .../tests/member-expression-call/input.tsx | 17 - .../no-deprecated-lifecycles/expected.tsx | 17 - .../tests/no-deprecated-lifecycles/input.tsx | 17 - .../tests/object-properties/expected.tsx | 17 - .../tests/object-properties/input.tsx | 17 - rename-unsafe-lifecycles/tsconfig.json | 17 - .../tests/act-in-async/expected.tsx | 6 - .../tests/act-in-async/input.tsx | 7 - .../tests/act-in-async/metrics.json | 11 - .../tests/export-named-act/expected.tsx | 1 - .../tests/export-named-act/input.tsx | 1 - .../tests/export-named-act/metrics.json | 11 - .../tests/multiple-act-calls/expected.tsx | 10 - .../tests/multiple-act-calls/input.tsx | 10 - .../tests/multiple-act-calls/metrics.json | 11 - .../tests/named-import/expected.tsx | 1 - .../react-testing-library-style/expected.tsx | 15 - .../react-testing-library-style/input.tsx | 15 - .../react-testing-library-style/metrics.json | 11 - .../tests/real-world-test-file/expected.tsx | 28 -- .../tests/real-world-test-file/input.tsx | 29 -- .../tests/real-world-test-file/metrics.json | 11 - .../tests/wildcard-import-2/expected.tsx | 4 - replace-act-import/tsconfig.json | 17 - replace-reactdom-render/README.md | 38 -- .../tests/multiple-render-calls/expected.tsx | 9 - .../tests/multiple-render-calls/input.tsx | 6 - .../tests/multiple-render-calls/metrics.json | 11 - .../tests/named-import/expected.tsx | 6 - .../tests/named-import/input.tsx | 4 - .../tests/no-reactdom-import/expected.tsx | 9 - .../tests/no-reactdom-import/input.tsx | 9 - .../tests/original-default/metrics.json | 11 - .../tests/original-nested/expected.tsx | 9 - .../tests/original-nested/metrics.json | 11 - .../tests/reactdom-member/expected.tsx | 6 - .../tests/reactdom-member/input.tsx | 4 - replace-reactdom-render/tsconfig.json | 17 - replace-string-ref/README.md | 38 -- .../tests/basic-string-ref/expected.tsx | 11 - .../tests/basic-string-ref/input.tsx | 9 - .../tests/basic-string-ref/metrics.json | 11 - .../tests/callback-ref-no-op/expected.tsx | 11 - .../tests/callback-ref-no-op/input.tsx | 11 - .../tests/expression-ref-no-op/expected.tsx | 10 - .../tests/expression-ref-no-op/input.tsx | 10 - .../tests/multiple-refs/expected.tsx | 21 -- .../tests/multiple-refs/input.tsx | 15 - .../tests/multiple-refs/metrics.json | 11 - .../tests/single-ref-in-fragment/expected.tsx | 16 - .../tests/single-ref-in-fragment/input.tsx | 14 - .../tests/single-ref-in-fragment/metrics.json | 11 - replace-string-ref/tsconfig.json | 17 - .../different-package-no-op/expected.tsx | 6 - .../tests/different-package-no-op/input.tsx | 6 - .../local-variable-shadow-no-op/expected.tsx | 23 -- .../local-variable-shadow-no-op/input.tsx | 23 -- .../local-variable-shadow-no-op/metrics.json | 11 - .../tests/multiple-usages/expected.tsx | 11 - .../tests/multiple-usages/input.tsx | 11 - .../tests/multiple-usages/metrics.json | 11 - .../real-world-form-component/expected.tsx | 40 --- .../tests/real-world-form-component/input.tsx | 40 --- .../real-world-form-component/metrics.json | 11 - replace-use-form-state/tsconfig.json | 17 - sort-comp/scripts/codemod.ts | 139 -------- sort-comp/tests/lifecycle-order/expected.tsx | 25 -- sort-comp/tests/lifecycle-order/input.tsx | 25 -- .../tests/no-reorder-needed/expected.tsx | 13 - sort-comp/tests/no-reorder-needed/input.tsx | 11 - .../tests/real-world-container/expected.tsx | 37 -- .../tests/real-world-container/input.tsx | 37 -- .../real-world-search-input/expected.tsx | 42 --- .../tests/real-world-search-input/input.tsx | 42 --- .../real-world-search-input/metrics.json | 10 - .../real-world-settings-panel/expected.tsx | 26 -- .../tests/real-world-settings-panel/input.tsx | 26 -- .../real-world-settings-panel/metrics.json | 10 - .../real-world-tabbed-panel/expected.tsx | 29 -- .../tests/real-world-tabbed-panel/input.tsx | 29 -- .../real-world-tabbed-panel/metrics.json | 10 - .../tests/render-helpers-order/expected.tsx | 26 -- .../tests/render-helpers-order/input.tsx | 26 -- .../tests/render-helpers-order/metrics.json | 10 - update-react-imports/README.md | 38 -- update-react-imports/scripts/codemod.ts | 143 -------- .../tests/fragment-only/expected.tsx | 11 - .../tests/fragment-only/input.tsx | 12 - .../tests/fragment-only/metrics.json | 12 - .../tests/jsx-only/expected.tsx | 6 - update-react-imports/tests/jsx-only/input.tsx | 7 - .../tests/jsx-only/metrics.json | 12 - .../tests/mixed-usage/expected.tsx | 9 - .../tests/mixed-usage/input.tsx | 9 - .../tests/mixed-usage/metrics.json | 12 - .../tests/no-react-import/expected.tsx | 8 - .../tests/no-react-import/input.tsx | 8 - .../tests/react-fragment-member/expected.tsx | 11 - .../tests/react-fragment-member/input.tsx | 11 - update-react-imports/tsconfig.json | 17 - use-context-hook/README.md | 38 -- .../tests/aliased-useContext/expected.tsx | 9 - .../tests/aliased-useContext/input.tsx | 9 - .../tests/aliased-useContext/metrics.json | 11 - .../tests/already-use/expected.tsx | 8 - use-context-hook/tests/already-use/input.tsx | 8 - .../tests/basic-named-import/expected.tsx | 9 - .../tests/basic-named-import/input.tsx | 9 - .../tests/basic-named-import/metrics.json | 11 - .../tests/mixed-import/expected.tsx | 13 - use-context-hook/tests/mixed-import/input.tsx | 13 - .../tests/no-context/expected.tsx | 7 - use-context-hook/tests/no-context/input.tsx | 7 - .../original-any-use-context/expected.tsx | 1 - .../tests/original-any-use-context/input.tsx | 1 - .../original-ts-use-context/metrics.json | 11 - .../useContext-not-from-react/expected.tsx | 9 - .../tests/useContext-not-from-react/input.tsx | 9 - use-context-hook/tsconfig.json | 17 - 652 files changed, 3960 insertions(+), 4103 deletions(-) create mode 100644 README.md create mode 100644 codemods/error-boundaries/README.md rename {error-boundaries => codemods/error-boundaries}/codemod.yaml (66%) rename {error-boundaries => codemods/error-boundaries}/package.json (77%) rename {error-boundaries => codemods/error-boundaries}/scripts/codemod.ts (90%) rename {error-boundaries/tests/original-class-component => codemods/error-boundaries/tests/class-component}/expected.tsx (100%) rename {error-boundaries/tests/original-class-component => codemods/error-boundaries/tests/class-component}/input.tsx (100%) rename {error-boundaries/tests/original-create-class => codemods/error-boundaries/tests/class-component}/metrics.json (63%) rename {error-boundaries/tests/original-create-class => codemods/error-boundaries/tests/create-class-component}/expected.tsx (100%) rename {error-boundaries/tests/original-create-class => codemods/error-boundaries/tests/create-class-component}/input.tsx (100%) rename {error-boundaries/tests/original-class-component => codemods/error-boundaries/tests/create-class-component}/metrics.json (62%) rename {error-boundaries => codemods/error-boundaries}/tsconfig.json (61%) rename {error-boundaries => codemods/error-boundaries}/workflow.yaml (100%) create mode 100644 codemods/find-dom-node/README.md rename {find-dom-node => codemods/find-dom-node}/codemod.yaml (67%) rename {find-dom-node => codemods/find-dom-node}/package.json (80%) rename {find-dom-node => codemods/find-dom-node}/scripts/codemod.ts (86%) create mode 100644 codemods/find-dom-node/tests/findDOMNode/expected.tsx create mode 100644 codemods/find-dom-node/tests/findDOMNode/input.tsx rename {find-dom-node/tests/nested-refs => codemods/find-dom-node/tests/findDOMNode}/metrics.json (57%) rename {pure-render-mixin => codemods/find-dom-node}/tsconfig.json (61%) rename {find-dom-node => codemods/find-dom-node}/workflow.yaml (100%) create mode 100644 codemods/manual-bind-to-arrow/README.md rename {manual-bind-to-arrow => codemods/manual-bind-to-arrow}/codemod.yaml (68%) rename {manual-bind-to-arrow => codemods/manual-bind-to-arrow}/package.json (77%) rename {manual-bind-to-arrow => codemods/manual-bind-to-arrow}/scripts/codemod.ts (58%) rename {manual-bind-to-arrow/tests/simple-bind => codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1}/expected.tsx (65%) rename {manual-bind-to-arrow/tests/simple-bind => codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1}/input.tsx (100%) rename {manual-bind-to-arrow/tests/simple-bind => codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1}/metrics.json (65%) create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/expected.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/input.tsx rename {manual-bind-to-arrow/tests/real-world-dropdown => codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10}/metrics.json (65%) create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/expected.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/input.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/expected.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/input.tsx rename {manual-bind-to-arrow/tests/constructor-with-props => codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2}/metrics.json (65%) rename {manual-bind-to-arrow/tests/bind-no-matching-method => codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3}/expected.tsx (100%) rename {manual-bind-to-arrow/tests/bind-no-matching-method => codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3}/input.tsx (100%) create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/expected.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/input.tsx rename {manual-bind-to-arrow/tests/bind-with-other-constructor-code => codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4}/metrics.json (61%) create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/expected.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/input.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/metrics.json create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/expected.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/input.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/metrics.json create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/expected.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/input.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/metrics.json create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/expected.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/input.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/metrics.json create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/expected.tsx create mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/input.tsx rename {manual-bind-to-arrow/tests/real-world-todo-item => codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9}/metrics.json (65%) create mode 100644 codemods/manual-bind-to-arrow/tsconfig.json rename {manual-bind-to-arrow => codemods/manual-bind-to-arrow}/workflow.yaml (100%) create mode 100644 codemods/pure-render-mixin/README.md rename {pure-render-mixin => codemods/pure-render-mixin}/codemod.yaml (64%) rename {pure-render-mixin => codemods/pure-render-mixin}/package.json (78%) rename {pure-render-mixin => codemods/pure-render-mixin}/scripts/codemod.ts (80%) rename {pure-render-mixin/tests/original-mixed-mixins => codemods/pure-render-mixin/tests/pure-render-mixin}/expected.tsx (99%) rename {pure-render-mixin/tests/original-mixed-mixins => codemods/pure-render-mixin/tests/pure-render-mixin}/input.tsx (100%) rename {pure-render-mixin/tests/original-mixed-mixins => codemods/pure-render-mixin/tests/pure-render-mixin}/metrics.json (65%) rename {pure-render-mixin/tests/original-single => codemods/pure-render-mixin/tests/pure-render-mixin2}/expected.tsx (100%) rename {pure-render-mixin/tests/original-single => codemods/pure-render-mixin/tests/pure-render-mixin2}/input.tsx (100%) rename {pure-render-mixin/tests/real-world-form => codemods/pure-render-mixin/tests/pure-render-mixin2}/metrics.json (66%) rename {pure-render-mixin/tests/original-fixture3-removes-var => codemods/pure-render-mixin/tests/pure-render-mixin3}/expected.tsx (100%) rename {pure-render-mixin/tests/original-fixture3-removes-var => codemods/pure-render-mixin/tests/pure-render-mixin3}/input.tsx (100%) rename {pure-render-mixin/tests/original-single => codemods/pure-render-mixin/tests/pure-render-mixin3}/metrics.json (66%) rename {pure-render-mixin/tests/create-class-direct-call => codemods/pure-render-mixin/tests/pure-render-mixin4}/expected.tsx (50%) create mode 100644 codemods/pure-render-mixin/tests/pure-render-mixin4/input.tsx rename {pure-render-mixin/tests/real-world-list-item => codemods/pure-render-mixin/tests/pure-render-mixin4}/metrics.json (65%) create mode 100644 codemods/pure-render-mixin/tests/pure-render-mixin4/test.config.json rename {react-proptypes-to-prop-types => codemods/pure-render-mixin}/tsconfig.json (61%) rename {replace-string-ref => codemods/pure-render-mixin}/workflow.yaml (66%) create mode 100644 codemods/react-proptypes-to-prop-types/README.md rename {react-proptypes-to-prop-types => codemods/react-proptypes-to-prop-types}/codemod.yaml (64%) rename {react-proptypes-to-prop-types => codemods/react-proptypes-to-prop-types}/package.json (77%) create mode 100644 codemods/react-proptypes-to-prop-types/scripts/codemod.ts create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-import/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-import/input.tsx rename {react-proptypes-to-prop-types/tests/basic-prop-types => codemods/react-proptypes-to-prop-types/tests/already-migrated-import}/metrics.json (63%) create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-require/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-require/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/already-migrated-require/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/default-and-named-import/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/default-import/input.tsx rename {react-proptypes-to-prop-types/tests/multiple-types => codemods/react-proptypes-to-prop-types/tests/default-import}/metrics.json (67%) create mode 100644 codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/import-alias/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/module-name/input.tsx rename {react-proptypes-to-prop-types/tests/array-and-object => codemods/react-proptypes-to-prop-types/tests/module-name}/metrics.json (55%) create mode 100644 codemods/react-proptypes-to-prop-types/tests/module-name/test.config.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/named-parameters/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/no-change-import/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/no-change-import/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/no-change-require/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/no-change-require/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require-alias/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require-destructured-only/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require/input.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/require/metrics.json create mode 100644 codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx create mode 100644 codemods/react-proptypes-to-prop-types/tests/with-top-comment/input.tsx rename {find-dom-node => codemods/react-proptypes-to-prop-types}/tsconfig.json (61%) rename {sort-comp => codemods/react-proptypes-to-prop-types}/workflow.yaml (65%) rename {rename-unsafe-lifecycles => codemods/rename-unsafe-lifecycles}/.gitignore (100%) create mode 100644 codemods/rename-unsafe-lifecycles/README.md rename {rename-unsafe-lifecycles => codemods/rename-unsafe-lifecycles}/codemod.yaml (69%) rename {rename-unsafe-lifecycles => codemods/rename-unsafe-lifecycles}/package.json (76%) rename {rename-unsafe-lifecycles => codemods/rename-unsafe-lifecycles}/pnpm-lock.yaml (100%) rename {rename-unsafe-lifecycles => codemods/rename-unsafe-lifecycles}/scripts/codemod.ts (93%) rename {rename-unsafe-lifecycles/tests/original-arrow-functions => codemods/rename-unsafe-lifecycles/tests/arrow-functions}/expected.tsx (99%) rename {rename-unsafe-lifecycles/tests/original-arrow-functions => codemods/rename-unsafe-lifecycles/tests/arrow-functions}/input.tsx (99%) rename {rename-unsafe-lifecycles/tests/class-methods => codemods/rename-unsafe-lifecycles/tests/arrow-functions}/metrics.json (78%) rename {rename-unsafe-lifecycles/tests/original-create-react-class => codemods/rename-unsafe-lifecycles/tests/create-react-class}/expected.tsx (99%) rename {rename-unsafe-lifecycles/tests/original-create-react-class => codemods/rename-unsafe-lifecycles/tests/create-react-class}/input.tsx (99%) rename {rename-unsafe-lifecycles/tests/original-create-react-class => codemods/rename-unsafe-lifecycles/tests/create-react-class}/metrics.json (74%) rename {rename-unsafe-lifecycles/tests/original-instance-methods => codemods/rename-unsafe-lifecycles/tests/instance-methods}/expected.tsx (99%) rename {rename-unsafe-lifecycles/tests/original-instance-methods => codemods/rename-unsafe-lifecycles/tests/instance-methods}/input.tsx (99%) rename {rename-unsafe-lifecycles/tests/original-arrow-functions => codemods/rename-unsafe-lifecycles/tests/instance-methods}/metrics.json (75%) create mode 100644 codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/expected.tsx create mode 100644 codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/input.tsx rename {rename-unsafe-lifecycles/tests/arrow-functions => codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles}/metrics.json (68%) create mode 100644 codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/expected.tsx create mode 100644 codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/input.tsx rename {rename-unsafe-lifecycles/tests/member-expression-call => codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods}/metrics.json (67%) create mode 100644 codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/expected.tsx create mode 100644 codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/input.tsx rename {rename-unsafe-lifecycles/tests/object-properties => codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another}/metrics.json (64%) create mode 100644 codemods/rename-unsafe-lifecycles/tests/standalone-function/expected.tsx create mode 100644 codemods/rename-unsafe-lifecycles/tests/standalone-function/input.tsx create mode 100644 codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/expected.tsx create mode 100644 codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/input.tsx rename {rename-unsafe-lifecycles/tests/original-instance-methods => codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx}/metrics.json (74%) create mode 100644 codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/expected.tsx create mode 100644 codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/input.tsx create mode 100644 codemods/rename-unsafe-lifecycles/tsconfig.json rename {pure-render-mixin => codemods/rename-unsafe-lifecycles}/workflow.yaml (100%) create mode 100644 codemods/replace-act-import/README.md rename {replace-act-import => codemods/replace-act-import}/codemod.yaml (66%) rename {replace-act-import => codemods/replace-act-import}/package.json (79%) rename {replace-act-import => codemods/replace-act-import}/scripts/codemod.ts (66%) rename {replace-act-import => codemods/replace-act-import}/tests/default-import/expected.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/default-import/input.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/default-import/metrics.json (100%) rename {replace-act-import => codemods/replace-act-import}/tests/named-import-2/expected.tsx (97%) rename {replace-act-import => codemods/replace-act-import}/tests/named-import-2/input.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/named-import-2/metrics.json (100%) create mode 100644 codemods/replace-act-import/tests/named-import/expected.tsx rename {replace-act-import => codemods/replace-act-import}/tests/named-import/input.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/named-import/metrics.json (100%) rename {replace-act-import => codemods/replace-act-import}/tests/other-import/expected.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/other-import/input.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/re-export/expected.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/re-export/input.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/re-export/metrics.json (100%) create mode 100644 codemods/replace-act-import/tests/wildcard-import-2/expected.tsx rename {replace-act-import => codemods/replace-act-import}/tests/wildcard-import-2/input.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/wildcard-import-2/metrics.json (100%) create mode 100644 codemods/replace-act-import/tests/wildcard-import/expected.tsx rename {replace-act-import => codemods/replace-act-import}/tests/wildcard-import/input.tsx (100%) rename {replace-act-import => codemods/replace-act-import}/tests/wildcard-import/metrics.json (100%) create mode 100644 codemods/replace-act-import/tsconfig.json rename {replace-act-import => codemods/replace-act-import}/workflow.yaml (100%) rename {replace-reactdom-render => codemods/replace-reactdom-render}/.gitignore (100%) create mode 100644 codemods/replace-reactdom-render/README.md rename {replace-reactdom-render => codemods/replace-reactdom-render}/codemod.yaml (68%) rename {replace-reactdom-render => codemods/replace-reactdom-render}/package.json (78%) rename {replace-reactdom-render => codemods/replace-reactdom-render}/pnpm-lock.yaml (100%) rename {replace-reactdom-render => codemods/replace-reactdom-render}/scripts/codemod.ts (81%) rename {replace-reactdom-render/tests/original-default => codemods/replace-reactdom-render/tests/default}/expected.tsx (59%) rename {replace-reactdom-render/tests/original-default => codemods/replace-reactdom-render/tests/default}/input.tsx (100%) rename {replace-reactdom-render/tests/reactdom-member => codemods/replace-reactdom-render/tests/default}/metrics.json (73%) create mode 100644 codemods/replace-reactdom-render/tests/nested/expected.tsx rename {replace-reactdom-render/tests/original-nested => codemods/replace-reactdom-render/tests/nested}/input.tsx (100%) rename {replace-reactdom-render/tests/named-import => codemods/replace-reactdom-render/tests/nested}/metrics.json (73%) create mode 100644 codemods/replace-reactdom-render/tsconfig.json rename {react-proptypes-to-prop-types => codemods/replace-reactdom-render}/workflow.yaml (100%) rename {replace-string-ref => codemods/replace-string-ref}/.gitignore (100%) create mode 100644 codemods/replace-string-ref/README.md rename {replace-string-ref => codemods/replace-string-ref}/codemod.yaml (70%) rename {replace-string-ref => codemods/replace-string-ref}/package.json (80%) rename {replace-string-ref => codemods/replace-string-ref}/pnpm-lock.yaml (100%) rename {replace-string-ref => codemods/replace-string-ref}/scripts/codemod.ts (77%) rename {replace-string-ref/tests/original-class-component-custom-import-names => codemods/replace-string-ref/tests/class-component-custom-import-names}/expected.tsx (71%) rename {replace-string-ref/tests/original-class-component-custom-import-names => codemods/replace-string-ref/tests/class-component-custom-import-names}/input.tsx (100%) rename {replace-string-ref/tests/original-class-component-named-import => codemods/replace-string-ref/tests/class-component-custom-import-names}/metrics.json (64%) rename {replace-string-ref/tests/original-class-component-default-import => codemods/replace-string-ref/tests/class-component-default-import}/expected.tsx (68%) rename {replace-string-ref/tests/original-class-component-default-import => codemods/replace-string-ref/tests/class-component-default-import}/input.tsx (100%) rename {replace-string-ref/tests/original-class-component-default-import => codemods/replace-string-ref/tests/class-component-default-import}/metrics.json (63%) rename {replace-string-ref/tests/original-class-component-named-import => codemods/replace-string-ref/tests/class-component-named-import}/expected.tsx (69%) rename {replace-string-ref/tests/original-class-component-named-import => codemods/replace-string-ref/tests/class-component-named-import}/input.tsx (100%) rename {replace-string-ref/tests/original-class-component-custom-import-names => codemods/replace-string-ref/tests/class-component-named-import}/metrics.json (62%) rename {replace-string-ref/tests/original-function-component => codemods/replace-string-ref/tests/function-component}/expected.tsx (100%) rename {replace-string-ref/tests/original-function-component => codemods/replace-string-ref/tests/function-component}/input.tsx (100%) create mode 100644 codemods/replace-string-ref/tsconfig.json rename {rename-unsafe-lifecycles => codemods/replace-string-ref}/workflow.yaml (100%) create mode 100644 codemods/replace-use-form-state/README.md rename {replace-use-form-state => codemods/replace-use-form-state}/codemod.yaml (63%) rename {replace-use-form-state => codemods/replace-use-form-state}/package.json (79%) rename {replace-use-form-state => codemods/replace-use-form-state}/scripts/codemod.ts (88%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/default-import/expected.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/default-import/input.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/default-import/metrics.json (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/named-import-2/expected.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/named-import-2/input.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/named-import-2/metrics.json (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/named-import-3/expected.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/named-import-3/input.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/named-import-3/metrics.json (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/named-import/expected.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/named-import/input.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/named-import/metrics.json (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/other-import/expected.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/other-import/input.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/wildcard-import/expected.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/wildcard-import/input.tsx (100%) rename {replace-use-form-state => codemods/replace-use-form-state}/tests/wildcard-import/metrics.json (100%) create mode 100644 codemods/replace-use-form-state/tsconfig.json rename {replace-use-form-state => codemods/replace-use-form-state}/workflow.yaml (100%) create mode 100644 codemods/sort-comp/README.md rename {sort-comp => codemods/sort-comp}/codemod.yaml (69%) rename {sort-comp => codemods/sort-comp}/package.json (79%) create mode 100644 codemods/sort-comp/scripts/codemod.ts create mode 100644 codemods/sort-comp/tests/custom-sort-custom-sort/expected.tsx create mode 100644 codemods/sort-comp/tests/custom-sort-custom-sort/input.tsx create mode 100644 codemods/sort-comp/tests/custom-sort-custom-sort/metrics.json create mode 100644 codemods/sort-comp/tests/custom-sort-custom-sort/test.config.json create mode 100644 codemods/sort-comp/tests/custom-sort-group-custom-sort-group/expected.tsx create mode 100644 codemods/sort-comp/tests/custom-sort-group-custom-sort-group/input.tsx create mode 100644 codemods/sort-comp/tests/custom-sort-group-custom-sort-group/metrics.json create mode 100644 codemods/sort-comp/tests/custom-sort-group-custom-sort-group/test.config.json create mode 100644 codemods/sort-comp/tests/sort-comp-pure/expected.tsx create mode 100644 codemods/sort-comp/tests/sort-comp-pure/input.tsx rename {sort-comp/tests/original-createClass => codemods/sort-comp/tests/sort-comp-pure}/metrics.json (55%) rename {sort-comp/tests/original-createClass => codemods/sort-comp/tests/sort-comp}/expected.tsx (94%) rename {sort-comp/tests/original-createClass => codemods/sort-comp/tests/sort-comp}/input.tsx (100%) rename {sort-comp/tests/real-world-container => codemods/sort-comp/tests/sort-comp}/metrics.json (55%) create mode 100644 codemods/sort-comp/tests/sort-comp2/expected.tsx create mode 100644 codemods/sort-comp/tests/sort-comp2/input.tsx rename {sort-comp/tests/lifecycle-order => codemods/sort-comp/tests/sort-comp2}/metrics.json (57%) create mode 100644 codemods/sort-comp/tests/sort-comp3/expected.tsx create mode 100644 codemods/sort-comp/tests/sort-comp3/input.tsx rename {sort-comp/tests/no-reorder-needed => codemods/sort-comp/tests/sort-comp3}/metrics.json (57%) rename {sort-comp => codemods/sort-comp}/tsconfig.json (51%) rename {update-react-imports => codemods/sort-comp}/workflow.yaml (64%) rename {update-react-imports => codemods/update-react-imports}/.gitignore (100%) create mode 100644 codemods/update-react-imports/README.md rename {update-react-imports => codemods/update-react-imports}/codemod.yaml (70%) rename {update-react-imports => codemods/update-react-imports}/package.json (79%) rename {update-react-imports => codemods/update-react-imports}/pnpm-lock.yaml (100%) create mode 100644 codemods/update-react-imports/scripts/codemod.ts create mode 100644 codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/expected.tsx create mode 100644 codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/input.tsx create mode 100644 codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/metrics.json create mode 100644 codemods/update-react-imports/tests/default-and-multiple-specifiers-import/expected.tsx create mode 100644 codemods/update-react-imports/tests/default-and-multiple-specifiers-import/input.tsx create mode 100644 codemods/update-react-imports/tests/default-and-multiple-specifiers-import/metrics.json rename {update-react-imports/tests/react-as-value => codemods/update-react-imports/tests/destructure-named-imports-react-not-removed}/expected.tsx (63%) rename update-react-imports/tests/original-react-not-removed/expected.tsx => codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/input.tsx (100%) create mode 100644 codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/metrics.json create mode 100644 codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/test.config.json create mode 100644 codemods/update-react-imports/tests/destructure-named-imports-variable-used/expected.tsx create mode 100644 codemods/update-react-imports/tests/destructure-named-imports-variable-used/input.tsx create mode 100644 codemods/update-react-imports/tests/destructure-named-imports-variable-used/metrics.json create mode 100644 codemods/update-react-imports/tests/destructure-named-imports-variable-used/test.config.json create mode 100644 codemods/update-react-imports/tests/destructure-named-imports/expected.tsx create mode 100644 codemods/update-react-imports/tests/destructure-named-imports/input.tsx rename {update-react-imports/tests/original-react-not-removed => codemods/update-react-imports/tests/destructure-named-imports}/metrics.json (57%) create mode 100644 codemods/update-react-imports/tests/destructure-named-imports/test.config.json create mode 100644 codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/expected.tsx create mode 100644 codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/input.tsx create mode 100644 codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/metrics.json create mode 100644 codemods/update-react-imports/tests/flow-default-and-type-specifier-import/expected.tsx create mode 100644 codemods/update-react-imports/tests/flow-default-and-type-specifier-import/input.tsx create mode 100644 codemods/update-react-imports/tests/flow-default-and-type-specifier-import/metrics.json create mode 100644 codemods/update-react-imports/tests/jsx-element/expected.tsx create mode 100644 codemods/update-react-imports/tests/jsx-element/input.tsx create mode 100644 codemods/update-react-imports/tests/jsx-element/metrics.json create mode 100644 codemods/update-react-imports/tests/jsx-fragment/expected.tsx create mode 100644 codemods/update-react-imports/tests/jsx-fragment/input.tsx create mode 100644 codemods/update-react-imports/tests/jsx-fragment/metrics.json create mode 100644 codemods/update-react-imports/tests/leading-comment/expected.tsx create mode 100644 codemods/update-react-imports/tests/leading-comment/input.tsx create mode 100644 codemods/update-react-imports/tests/leading-comment/metrics.json create mode 100644 codemods/update-react-imports/tests/react-already-used-named-export/expected.tsx create mode 100644 codemods/update-react-imports/tests/react-already-used-named-export/input.tsx create mode 100644 codemods/update-react-imports/tests/react-already-used-named-export/metrics.json rename {update-react-imports/tests/member-expression => codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable}/expected.tsx (54%) rename {update-react-imports/tests/member-expression => codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable}/input.tsx (53%) create mode 100644 codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/metrics.json create mode 100644 codemods/update-react-imports/tests/react-basic-default-export-jsx-element/expected.tsx rename replace-act-import/tests/wildcard-import/expected.tsx => codemods/update-react-imports/tests/react-basic-default-export-jsx-element/input.tsx (65%) create mode 100644 codemods/update-react-imports/tests/react-basic-default-export-jsx-element/metrics.json rename {update-react-imports/tests/original-react-basic-default-export => codemods/update-react-imports/tests/react-basic-default-export}/expected.tsx (98%) rename {update-react-imports/tests/original-react-basic-default-export => codemods/update-react-imports/tests/react-basic-default-export}/input.tsx (100%) rename {update-react-imports/tests/member-expression => codemods/update-react-imports/tests/react-basic-default-export}/metrics.json (60%) create mode 100644 codemods/update-react-imports/tests/react-jsx-member-expression/expected.tsx create mode 100644 codemods/update-react-imports/tests/react-jsx-member-expression/input.tsx rename {update-react-imports/tests/original-react-basic-default-export => codemods/update-react-imports/tests/react-jsx-member-expression}/metrics.json (57%) create mode 100644 codemods/update-react-imports/tests/react-not-removed/expected.tsx rename {update-react-imports/tests/original-react-not-removed => codemods/update-react-imports/tests/react-not-removed}/input.tsx (86%) rename {update-react-imports/tests/react-as-value => codemods/update-react-imports/tests/react-not-removed}/metrics.json (60%) create mode 100644 codemods/update-react-imports/tests/react-type-default-export/expected.tsx create mode 100644 codemods/update-react-imports/tests/react-type-default-export/input.tsx create mode 100644 codemods/update-react-imports/tests/react-type-default-export/metrics.json create mode 100644 codemods/update-react-imports/tests/react-type-not-removed/expected.tsx create mode 100644 codemods/update-react-imports/tests/react-type-not-removed/input.tsx create mode 100644 codemods/update-react-imports/tests/react-type-not-removed/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-jsx-element-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-jsx-element-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-jsx-element-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-leading-comment-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-leading-comment-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-leading-comment-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/input.tsx rename {update-react-imports/tests/react-fragment-member => codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx}/metrics.json (50%) create mode 100644 codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-react-not-removed-tsx/expected.tsx rename {update-react-imports/tests/react-as-value => codemods/update-react-imports/tests/typescript-react-not-removed-tsx}/input.tsx (62%) create mode 100644 codemods/update-react-imports/tests/typescript-react-not-removed-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/typescript-variable-already-used-tsx/expected.tsx create mode 100644 codemods/update-react-imports/tests/typescript-variable-already-used-tsx/input.tsx create mode 100644 codemods/update-react-imports/tests/typescript-variable-already-used-tsx/metrics.json create mode 100644 codemods/update-react-imports/tests/variable-already-used/expected.tsx create mode 100644 codemods/update-react-imports/tests/variable-already-used/input.tsx create mode 100644 codemods/update-react-imports/tests/variable-already-used/metrics.json create mode 100644 codemods/update-react-imports/tsconfig.json rename {use-context-hook => codemods/update-react-imports}/workflow.yaml (60%) rename {use-context-hook => codemods/use-context-hook}/.gitignore (100%) create mode 100644 codemods/use-context-hook/README.md rename {use-context-hook => codemods/use-context-hook}/codemod.yaml (70%) rename {use-context-hook => codemods/use-context-hook}/package.json (80%) rename {use-context-hook => codemods/use-context-hook}/pnpm-lock.yaml (100%) rename {use-context-hook => codemods/use-context-hook}/scripts/codemod.ts (92%) create mode 100644 codemods/use-context-hook/tests/any-use-context/expected.tsx create mode 100644 codemods/use-context-hook/tests/any-use-context/input.tsx rename {use-context-hook/tests/original-mixed-import => codemods/use-context-hook/tests/mixed-import}/expected.tsx (70%) rename {use-context-hook/tests/original-mixed-import => codemods/use-context-hook/tests/mixed-import}/input.tsx (68%) rename {use-context-hook => codemods/use-context-hook}/tests/mixed-import/metrics.json (100%) create mode 100644 codemods/use-context-hook/tests/typescript-any-use-context/expected.tsx create mode 100644 codemods/use-context-hook/tests/typescript-any-use-context/input.tsx rename {use-context-hook/tests/react-member-expression => codemods/use-context-hook/tests/typescript-use-context-2}/expected.tsx (51%) rename {use-context-hook/tests/react-member-expression => codemods/use-context-hook/tests/typescript-use-context-2}/input.tsx (51%) rename {use-context-hook/tests/react-member-expression => codemods/use-context-hook/tests/typescript-use-context-2}/metrics.json (61%) rename {use-context-hook/tests/original-ts-use-context => codemods/use-context-hook/tests/typescript-use-context}/expected.tsx (98%) rename {use-context-hook/tests/original-ts-use-context => codemods/use-context-hook/tests/typescript-use-context}/input.tsx (98%) rename {use-context-hook/tests/original-mixed-import => codemods/use-context-hook/tests/typescript-use-context}/metrics.json (69%) rename {use-context-hook/tests/original-use-context-2 => codemods/use-context-hook/tests/use-context-2}/expected.tsx (64%) rename {use-context-hook/tests/original-use-context-2 => codemods/use-context-hook/tests/use-context-2}/input.tsx (60%) rename {use-context-hook/tests/original-use-context-2 => codemods/use-context-hook/tests/use-context-2}/metrics.json (70%) rename {use-context-hook/tests/original-use-context => codemods/use-context-hook/tests/use-context}/expected.tsx (68%) rename {use-context-hook/tests/original-use-context => codemods/use-context-hook/tests/use-context}/input.tsx (66%) rename {use-context-hook/tests/original-use-context => codemods/use-context-hook/tests/use-context}/metrics.json (70%) create mode 100644 codemods/use-context-hook/tsconfig.json rename {replace-reactdom-render => codemods/use-context-hook}/workflow.yaml (100%) delete mode 100644 error-boundaries/tests/arrow-assignment/expected.tsx delete mode 100644 error-boundaries/tests/arrow-assignment/input.tsx delete mode 100644 error-boundaries/tests/arrow-assignment/metrics.json delete mode 100644 error-boundaries/tests/export-default-class/expected.tsx delete mode 100644 error-boundaries/tests/export-default-class/input.tsx delete mode 100644 error-boundaries/tests/export-default-class/metrics.json delete mode 100644 error-boundaries/tests/mixed-methods/expected.tsx delete mode 100644 error-boundaries/tests/mixed-methods/input.tsx delete mode 100644 error-boundaries/tests/mixed-methods/metrics.json delete mode 100644 error-boundaries/tests/no-unstable-handleError/expected.tsx delete mode 100644 error-boundaries/tests/no-unstable-handleError/input.tsx delete mode 100644 error-boundaries/tests/object-literal-method/expected.tsx delete mode 100644 error-boundaries/tests/object-literal-method/input.tsx delete mode 100644 error-boundaries/tests/real-world-dashboard-error-boundary/expected.tsx delete mode 100644 error-boundaries/tests/real-world-dashboard-error-boundary/input.tsx delete mode 100644 error-boundaries/tests/real-world-dashboard-error-boundary/metrics.json delete mode 100644 error-boundaries/tests/similar-name-no-op/expected.tsx delete mode 100644 error-boundaries/tests/similar-name-no-op/input.tsx delete mode 100644 error-boundaries/tests/single-method/expected.tsx delete mode 100644 error-boundaries/tests/single-method/input.tsx delete mode 100644 error-boundaries/tests/single-method/metrics.json delete mode 100644 error-boundaries/tests/three-classes/expected.tsx delete mode 100644 error-boundaries/tests/three-classes/input.tsx delete mode 100644 error-boundaries/tests/three-classes/metrics.json delete mode 100644 error-boundaries/tests/typescript-method/expected.tsx delete mode 100644 error-boundaries/tests/typescript-method/input.tsx delete mode 100644 error-boundaries/tests/typescript-method/metrics.json delete mode 100644 find-dom-node/tests/comment-and-string-unchanged/expected.tsx delete mode 100644 find-dom-node/tests/comment-and-string-unchanged/input.tsx delete mode 100644 find-dom-node/tests/enzyme-wrapper-style/expected.tsx delete mode 100644 find-dom-node/tests/enzyme-wrapper-style/input.tsx delete mode 100644 find-dom-node/tests/enzyme-wrapper-style/metrics.json delete mode 100644 find-dom-node/tests/inside-another-call/expected.tsx delete mode 100644 find-dom-node/tests/inside-another-call/input.tsx delete mode 100644 find-dom-node/tests/inside-another-call/metrics.json delete mode 100644 find-dom-node/tests/multiple-calls/expected.tsx delete mode 100644 find-dom-node/tests/multiple-calls/input.tsx delete mode 100644 find-dom-node/tests/multiple-calls/metrics.json delete mode 100644 find-dom-node/tests/nested-refs/expected.tsx delete mode 100644 find-dom-node/tests/nested-refs/input.tsx delete mode 100644 find-dom-node/tests/no-getDOMNode/expected.tsx delete mode 100644 find-dom-node/tests/no-getDOMNode/input.tsx delete mode 100644 find-dom-node/tests/react-import-alias/expected.tsx delete mode 100644 find-dom-node/tests/react-import-alias/input.tsx delete mode 100644 find-dom-node/tests/react-import-alias/metrics.json delete mode 100644 find-dom-node/tests/real-world-autosize-input/expected.tsx delete mode 100644 find-dom-node/tests/real-world-autosize-input/input.tsx delete mode 100644 find-dom-node/tests/real-world-autosize-input/metrics.json delete mode 100644 find-dom-node/tests/refs-getDOMNode/expected.tsx delete mode 100644 find-dom-node/tests/refs-getDOMNode/input.tsx delete mode 100644 find-dom-node/tests/refs-getDOMNode/metrics.json delete mode 100644 find-dom-node/tests/this-getDOMNode/expected.tsx delete mode 100644 find-dom-node/tests/this-getDOMNode/input.tsx delete mode 100644 find-dom-node/tests/this-getDOMNode/metrics.json delete mode 100644 find-dom-node/tests/two-components-same-file/expected.tsx delete mode 100644 find-dom-node/tests/two-components-same-file/input.tsx delete mode 100644 find-dom-node/tests/two-components-same-file/metrics.json delete mode 100644 find-dom-node/tests/variable-holding-ref/expected.tsx delete mode 100644 find-dom-node/tests/variable-holding-ref/input.tsx delete mode 100644 find-dom-node/tests/variable-holding-ref/metrics.json delete mode 100644 manual-bind-to-arrow/tests/bind-outside-constructor-no-op/expected.tsx delete mode 100644 manual-bind-to-arrow/tests/bind-outside-constructor-no-op/input.tsx delete mode 100644 manual-bind-to-arrow/tests/bind-with-other-constructor-code/expected.tsx delete mode 100644 manual-bind-to-arrow/tests/bind-with-other-constructor-code/input.tsx delete mode 100644 manual-bind-to-arrow/tests/constructor-with-props/expected.tsx delete mode 100644 manual-bind-to-arrow/tests/constructor-with-props/input.tsx delete mode 100644 manual-bind-to-arrow/tests/no-bind/expected.tsx delete mode 100644 manual-bind-to-arrow/tests/no-bind/input.tsx delete mode 100644 manual-bind-to-arrow/tests/real-world-dropdown/expected.tsx delete mode 100644 manual-bind-to-arrow/tests/real-world-dropdown/input.tsx delete mode 100644 manual-bind-to-arrow/tests/real-world-form-two-binds/expected.tsx delete mode 100644 manual-bind-to-arrow/tests/real-world-form-two-binds/input.tsx delete mode 100644 manual-bind-to-arrow/tests/real-world-form-two-binds/metrics.json delete mode 100644 manual-bind-to-arrow/tests/real-world-todo-item/expected.tsx delete mode 100644 manual-bind-to-arrow/tests/real-world-todo-item/input.tsx delete mode 100644 manual-bind-to-arrow/tsconfig.json delete mode 100644 pure-render-mixin/tests/create-class-direct-call/input.tsx delete mode 100644 pure-render-mixin/tests/create-class-direct-call/metrics.json delete mode 100644 pure-render-mixin/tests/no-pure-render-mixin/expected.tsx delete mode 100644 pure-render-mixin/tests/no-pure-render-mixin/input.tsx delete mode 100644 pure-render-mixin/tests/original-fixture3-removes-var/metrics.json delete mode 100644 pure-render-mixin/tests/real-world-form/expected.tsx delete mode 100644 pure-render-mixin/tests/real-world-form/input.tsx delete mode 100644 pure-render-mixin/tests/real-world-list-item/expected.tsx delete mode 100644 pure-render-mixin/tests/real-world-list-item/input.tsx delete mode 100644 pure-render-mixin/tests/real-world-modal-with-other-mixin/expected.tsx delete mode 100644 pure-render-mixin/tests/real-world-modal-with-other-mixin/input.tsx delete mode 100644 pure-render-mixin/tests/real-world-modal-with-other-mixin/metrics.json delete mode 100644 pure-render-mixin/tests/two-components-one-with-mixin/expected.tsx delete mode 100644 pure-render-mixin/tests/two-components-one-with-mixin/input.tsx delete mode 100644 pure-render-mixin/tests/two-components-one-with-mixin/metrics.json delete mode 100644 react-proptypes-to-prop-types/scripts/codemod.ts delete mode 100644 react-proptypes-to-prop-types/tests/array-and-object/expected.tsx delete mode 100644 react-proptypes-to-prop-types/tests/array-and-object/input.tsx delete mode 100644 react-proptypes-to-prop-types/tests/basic-prop-types/expected.tsx delete mode 100644 react-proptypes-to-prop-types/tests/basic-prop-types/input.tsx delete mode 100644 react-proptypes-to-prop-types/tests/multiple-types/expected.tsx delete mode 100644 react-proptypes-to-prop-types/tests/multiple-types/input.tsx delete mode 100644 react-proptypes-to-prop-types/tests/no-react-proptypes/expected.tsx delete mode 100644 react-proptypes-to-prop-types/tests/no-react-proptypes/input.tsx delete mode 100644 rename-unsafe-lifecycles/README.md delete mode 100644 rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx delete mode 100644 rename-unsafe-lifecycles/tests/arrow-functions/input.tsx delete mode 100644 rename-unsafe-lifecycles/tests/class-methods/expected.tsx delete mode 100644 rename-unsafe-lifecycles/tests/class-methods/input.tsx delete mode 100644 rename-unsafe-lifecycles/tests/componentWillUpdate-only/expected.tsx delete mode 100644 rename-unsafe-lifecycles/tests/componentWillUpdate-only/input.tsx delete mode 100644 rename-unsafe-lifecycles/tests/componentWillUpdate-only/metrics.json delete mode 100644 rename-unsafe-lifecycles/tests/member-expression-call/expected.tsx delete mode 100644 rename-unsafe-lifecycles/tests/member-expression-call/input.tsx delete mode 100644 rename-unsafe-lifecycles/tests/no-deprecated-lifecycles/expected.tsx delete mode 100644 rename-unsafe-lifecycles/tests/no-deprecated-lifecycles/input.tsx delete mode 100644 rename-unsafe-lifecycles/tests/object-properties/expected.tsx delete mode 100644 rename-unsafe-lifecycles/tests/object-properties/input.tsx delete mode 100644 rename-unsafe-lifecycles/tsconfig.json delete mode 100644 replace-act-import/tests/act-in-async/expected.tsx delete mode 100644 replace-act-import/tests/act-in-async/input.tsx delete mode 100644 replace-act-import/tests/act-in-async/metrics.json delete mode 100644 replace-act-import/tests/export-named-act/expected.tsx delete mode 100644 replace-act-import/tests/export-named-act/input.tsx delete mode 100644 replace-act-import/tests/export-named-act/metrics.json delete mode 100644 replace-act-import/tests/multiple-act-calls/expected.tsx delete mode 100644 replace-act-import/tests/multiple-act-calls/input.tsx delete mode 100644 replace-act-import/tests/multiple-act-calls/metrics.json delete mode 100644 replace-act-import/tests/named-import/expected.tsx delete mode 100644 replace-act-import/tests/react-testing-library-style/expected.tsx delete mode 100644 replace-act-import/tests/react-testing-library-style/input.tsx delete mode 100644 replace-act-import/tests/react-testing-library-style/metrics.json delete mode 100644 replace-act-import/tests/real-world-test-file/expected.tsx delete mode 100644 replace-act-import/tests/real-world-test-file/input.tsx delete mode 100644 replace-act-import/tests/real-world-test-file/metrics.json delete mode 100644 replace-act-import/tests/wildcard-import-2/expected.tsx delete mode 100644 replace-act-import/tsconfig.json delete mode 100644 replace-reactdom-render/README.md delete mode 100644 replace-reactdom-render/tests/multiple-render-calls/expected.tsx delete mode 100644 replace-reactdom-render/tests/multiple-render-calls/input.tsx delete mode 100644 replace-reactdom-render/tests/multiple-render-calls/metrics.json delete mode 100644 replace-reactdom-render/tests/named-import/expected.tsx delete mode 100644 replace-reactdom-render/tests/named-import/input.tsx delete mode 100644 replace-reactdom-render/tests/no-reactdom-import/expected.tsx delete mode 100644 replace-reactdom-render/tests/no-reactdom-import/input.tsx delete mode 100644 replace-reactdom-render/tests/original-default/metrics.json delete mode 100644 replace-reactdom-render/tests/original-nested/expected.tsx delete mode 100644 replace-reactdom-render/tests/original-nested/metrics.json delete mode 100644 replace-reactdom-render/tests/reactdom-member/expected.tsx delete mode 100644 replace-reactdom-render/tests/reactdom-member/input.tsx delete mode 100644 replace-reactdom-render/tsconfig.json delete mode 100644 replace-string-ref/README.md delete mode 100644 replace-string-ref/tests/basic-string-ref/expected.tsx delete mode 100644 replace-string-ref/tests/basic-string-ref/input.tsx delete mode 100644 replace-string-ref/tests/basic-string-ref/metrics.json delete mode 100644 replace-string-ref/tests/callback-ref-no-op/expected.tsx delete mode 100644 replace-string-ref/tests/callback-ref-no-op/input.tsx delete mode 100644 replace-string-ref/tests/expression-ref-no-op/expected.tsx delete mode 100644 replace-string-ref/tests/expression-ref-no-op/input.tsx delete mode 100644 replace-string-ref/tests/multiple-refs/expected.tsx delete mode 100644 replace-string-ref/tests/multiple-refs/input.tsx delete mode 100644 replace-string-ref/tests/multiple-refs/metrics.json delete mode 100644 replace-string-ref/tests/single-ref-in-fragment/expected.tsx delete mode 100644 replace-string-ref/tests/single-ref-in-fragment/input.tsx delete mode 100644 replace-string-ref/tests/single-ref-in-fragment/metrics.json delete mode 100644 replace-string-ref/tsconfig.json delete mode 100644 replace-use-form-state/tests/different-package-no-op/expected.tsx delete mode 100644 replace-use-form-state/tests/different-package-no-op/input.tsx delete mode 100644 replace-use-form-state/tests/local-variable-shadow-no-op/expected.tsx delete mode 100644 replace-use-form-state/tests/local-variable-shadow-no-op/input.tsx delete mode 100644 replace-use-form-state/tests/local-variable-shadow-no-op/metrics.json delete mode 100644 replace-use-form-state/tests/multiple-usages/expected.tsx delete mode 100644 replace-use-form-state/tests/multiple-usages/input.tsx delete mode 100644 replace-use-form-state/tests/multiple-usages/metrics.json delete mode 100644 replace-use-form-state/tests/real-world-form-component/expected.tsx delete mode 100644 replace-use-form-state/tests/real-world-form-component/input.tsx delete mode 100644 replace-use-form-state/tests/real-world-form-component/metrics.json delete mode 100644 replace-use-form-state/tsconfig.json delete mode 100644 sort-comp/scripts/codemod.ts delete mode 100644 sort-comp/tests/lifecycle-order/expected.tsx delete mode 100644 sort-comp/tests/lifecycle-order/input.tsx delete mode 100644 sort-comp/tests/no-reorder-needed/expected.tsx delete mode 100644 sort-comp/tests/no-reorder-needed/input.tsx delete mode 100644 sort-comp/tests/real-world-container/expected.tsx delete mode 100644 sort-comp/tests/real-world-container/input.tsx delete mode 100644 sort-comp/tests/real-world-search-input/expected.tsx delete mode 100644 sort-comp/tests/real-world-search-input/input.tsx delete mode 100644 sort-comp/tests/real-world-search-input/metrics.json delete mode 100644 sort-comp/tests/real-world-settings-panel/expected.tsx delete mode 100644 sort-comp/tests/real-world-settings-panel/input.tsx delete mode 100644 sort-comp/tests/real-world-settings-panel/metrics.json delete mode 100644 sort-comp/tests/real-world-tabbed-panel/expected.tsx delete mode 100644 sort-comp/tests/real-world-tabbed-panel/input.tsx delete mode 100644 sort-comp/tests/real-world-tabbed-panel/metrics.json delete mode 100644 sort-comp/tests/render-helpers-order/expected.tsx delete mode 100644 sort-comp/tests/render-helpers-order/input.tsx delete mode 100644 sort-comp/tests/render-helpers-order/metrics.json delete mode 100644 update-react-imports/README.md delete mode 100644 update-react-imports/scripts/codemod.ts delete mode 100644 update-react-imports/tests/fragment-only/expected.tsx delete mode 100644 update-react-imports/tests/fragment-only/input.tsx delete mode 100644 update-react-imports/tests/fragment-only/metrics.json delete mode 100644 update-react-imports/tests/jsx-only/expected.tsx delete mode 100644 update-react-imports/tests/jsx-only/input.tsx delete mode 100644 update-react-imports/tests/jsx-only/metrics.json delete mode 100644 update-react-imports/tests/mixed-usage/expected.tsx delete mode 100644 update-react-imports/tests/mixed-usage/input.tsx delete mode 100644 update-react-imports/tests/mixed-usage/metrics.json delete mode 100644 update-react-imports/tests/no-react-import/expected.tsx delete mode 100644 update-react-imports/tests/no-react-import/input.tsx delete mode 100644 update-react-imports/tests/react-fragment-member/expected.tsx delete mode 100644 update-react-imports/tests/react-fragment-member/input.tsx delete mode 100644 update-react-imports/tsconfig.json delete mode 100644 use-context-hook/README.md delete mode 100644 use-context-hook/tests/aliased-useContext/expected.tsx delete mode 100644 use-context-hook/tests/aliased-useContext/input.tsx delete mode 100644 use-context-hook/tests/aliased-useContext/metrics.json delete mode 100644 use-context-hook/tests/already-use/expected.tsx delete mode 100644 use-context-hook/tests/already-use/input.tsx delete mode 100644 use-context-hook/tests/basic-named-import/expected.tsx delete mode 100644 use-context-hook/tests/basic-named-import/input.tsx delete mode 100644 use-context-hook/tests/basic-named-import/metrics.json delete mode 100644 use-context-hook/tests/mixed-import/expected.tsx delete mode 100644 use-context-hook/tests/mixed-import/input.tsx delete mode 100644 use-context-hook/tests/no-context/expected.tsx delete mode 100644 use-context-hook/tests/no-context/input.tsx delete mode 100644 use-context-hook/tests/original-any-use-context/expected.tsx delete mode 100644 use-context-hook/tests/original-any-use-context/input.tsx delete mode 100644 use-context-hook/tests/original-ts-use-context/metrics.json delete mode 100644 use-context-hook/tests/useContext-not-from-react/expected.tsx delete mode 100644 use-context-hook/tests/useContext-not-from-react/input.tsx delete mode 100644 use-context-hook/tsconfig.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..d8c9916 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# jssg-react-codemods + +React codemods built with JSSG and JavaScript ast-grep. + +## Packages + +- `@react/error-boundaries` +- `@react/find-dom-node` +- `@react/manual-bind-to-arrow` +- `@react/pure-render-mixin` +- `@react/react-proptypes-to-prop-types` +- `@react/rename-unsafe-lifecycles` +- `@react/replace-act-import` +- `@react/replace-reactdom-render` +- `@react/replace-string-ref` +- `@react/replace-use-form-state` +- `@react/sort-comp` +- `@react/update-react-imports` +- `@react/use-context-hook` + +## Development + +```bash +pnpm test +pnpm -r check-types +``` + +Run a single package: + +```bash +pnpm --filter @react-codemods/replace-reactdom-render test +``` diff --git a/codemods/error-boundaries/README.md b/codemods/error-boundaries/README.md new file mode 100644 index 0000000..7f185ba --- /dev/null +++ b/codemods/error-boundaries/README.md @@ -0,0 +1,16 @@ +# @react/error-boundaries + +Rename `unstable_handleError` to `componentDidCatch` in React class components and `createClass` component definitions. + +## Usage + +```bash +codemod run @react/error-boundaries +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/error-boundaries/codemod.yaml b/codemods/error-boundaries/codemod.yaml similarity index 66% rename from error-boundaries/codemod.yaml rename to codemods/error-boundaries/codemod.yaml index 8756d6d..bb02200 100644 --- a/error-boundaries/codemod.yaml +++ b/codemods/error-boundaries/codemod.yaml @@ -1,16 +1,16 @@ schema_version: "1.0" -name: "@react-codemods/error-boundaries" +name: "@react/error-boundaries" version: "0.1.0" description: "Rename unstable_handleError to componentDidCatch" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration", "error-boundary"] +keywords: ["React"] registry: access: "public" diff --git a/error-boundaries/package.json b/codemods/error-boundaries/package.json similarity index 77% rename from error-boundaries/package.json rename to codemods/error-boundaries/package.json index 02f1bfb..7c0d19c 100644 --- a/error-boundaries/package.json +++ b/codemods/error-boundaries/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/error-boundaries", + "name": "@react/error-boundaries", "version": "0.1.0", "description": "Rename unstable_handleError to componentDidCatch", "type": "module", @@ -9,6 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" } } diff --git a/error-boundaries/scripts/codemod.ts b/codemods/error-boundaries/scripts/codemod.ts similarity index 90% rename from error-boundaries/scripts/codemod.ts rename to codemods/error-boundaries/scripts/codemod.ts index c5a03d8..79c8434 100644 --- a/error-boundaries/scripts/codemod.ts +++ b/codemods/error-boundaries/scripts/codemod.ts @@ -2,6 +2,12 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + + const transform: Transform = async (root) => { const rootNode = root.root(); const edits: Edit[] = []; @@ -68,7 +74,7 @@ const transform: Transform = async (root) => { if (edits.length > 0) { renameMetric.increment( { - file: root.filename(), + file: metricFile(root.filename()), }, edits.length, ); diff --git a/error-boundaries/tests/original-class-component/expected.tsx b/codemods/error-boundaries/tests/class-component/expected.tsx similarity index 100% rename from error-boundaries/tests/original-class-component/expected.tsx rename to codemods/error-boundaries/tests/class-component/expected.tsx diff --git a/error-boundaries/tests/original-class-component/input.tsx b/codemods/error-boundaries/tests/class-component/input.tsx similarity index 100% rename from error-boundaries/tests/original-class-component/input.tsx rename to codemods/error-boundaries/tests/class-component/input.tsx diff --git a/error-boundaries/tests/original-create-class/metrics.json b/codemods/error-boundaries/tests/class-component/metrics.json similarity index 63% rename from error-boundaries/tests/original-create-class/metrics.json rename to codemods/error-boundaries/tests/class-component/metrics.json index 70c49ee..ce4a6fe 100644 --- a/error-boundaries/tests/original-create-class/metrics.json +++ b/codemods/error-boundaries/tests/class-component/metrics.json @@ -2,7 +2,7 @@ "error-boundary-renames": [ { "cardinality": { - "file": "tests/original-create-class/input.tsx" + "file": "tests/class-component/input.tsx" }, "count": 2 } diff --git a/error-boundaries/tests/original-create-class/expected.tsx b/codemods/error-boundaries/tests/create-class-component/expected.tsx similarity index 100% rename from error-boundaries/tests/original-create-class/expected.tsx rename to codemods/error-boundaries/tests/create-class-component/expected.tsx diff --git a/error-boundaries/tests/original-create-class/input.tsx b/codemods/error-boundaries/tests/create-class-component/input.tsx similarity index 100% rename from error-boundaries/tests/original-create-class/input.tsx rename to codemods/error-boundaries/tests/create-class-component/input.tsx diff --git a/error-boundaries/tests/original-class-component/metrics.json b/codemods/error-boundaries/tests/create-class-component/metrics.json similarity index 62% rename from error-boundaries/tests/original-class-component/metrics.json rename to codemods/error-boundaries/tests/create-class-component/metrics.json index 08958b3..21136e9 100644 --- a/error-boundaries/tests/original-class-component/metrics.json +++ b/codemods/error-boundaries/tests/create-class-component/metrics.json @@ -2,7 +2,7 @@ "error-boundary-renames": [ { "cardinality": { - "file": "tests/original-class-component/input.tsx" + "file": "tests/create-class-component/input.tsx" }, "count": 2 } diff --git a/error-boundaries/tsconfig.json b/codemods/error-boundaries/tsconfig.json similarity index 61% rename from error-boundaries/tsconfig.json rename to codemods/error-boundaries/tsconfig.json index 469fc5a..decec57 100644 --- a/error-boundaries/tsconfig.json +++ b/codemods/error-boundaries/tsconfig.json @@ -1,11 +1,19 @@ { "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], "module": "NodeNext", "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], + "types": [ + "@codemod.com/jssg-types", + "node" + ], "allowImportingTsExtensions": true, "noEmit": true, - "verbatimModuleSyntax": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, "erasableSyntaxOnly": true, "strict": true, "strictNullChecks": true, @@ -13,5 +21,7 @@ "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true }, - "exclude": ["tests"] + "exclude": [ + "tests" + ] } diff --git a/error-boundaries/workflow.yaml b/codemods/error-boundaries/workflow.yaml similarity index 100% rename from error-boundaries/workflow.yaml rename to codemods/error-boundaries/workflow.yaml diff --git a/codemods/find-dom-node/README.md b/codemods/find-dom-node/README.md new file mode 100644 index 0000000..222ed0c --- /dev/null +++ b/codemods/find-dom-node/README.md @@ -0,0 +1,16 @@ +# @react/find-dom-node + +Replace `.getDOMNode()` calls with `React.findDOMNode(...)`. + +## Usage + +```bash +codemod run @react/find-dom-node +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/find-dom-node/codemod.yaml b/codemods/find-dom-node/codemod.yaml similarity index 67% rename from find-dom-node/codemod.yaml rename to codemods/find-dom-node/codemod.yaml index 374db93..51f9a8e 100644 --- a/find-dom-node/codemod.yaml +++ b/codemods/find-dom-node/codemod.yaml @@ -1,16 +1,16 @@ schema_version: "1.0" -name: "@react-codemods/find-dom-node" +name: "@react/find-dom-node" version: "0.1.0" description: "Replace getDOMNode() with React.findDOMNode()" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration", "findDOMNode"] +keywords: ["React"] registry: access: "public" diff --git a/find-dom-node/package.json b/codemods/find-dom-node/package.json similarity index 80% rename from find-dom-node/package.json rename to codemods/find-dom-node/package.json index 8fed40c..de25953 100644 --- a/find-dom-node/package.json +++ b/codemods/find-dom-node/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/find-dom-node", + "name": "@react/find-dom-node", "version": "0.1.0", "description": "Replace getDOMNode() with React.findDOMNode()", "type": "module", @@ -9,7 +9,8 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" }, "dependencies": { "@jssg/utils": "^0.0.2" diff --git a/find-dom-node/scripts/codemod.ts b/codemods/find-dom-node/scripts/codemod.ts similarity index 86% rename from find-dom-node/scripts/codemod.ts rename to codemods/find-dom-node/scripts/codemod.ts index 6f9e28f..b5584df 100644 --- a/find-dom-node/scripts/codemod.ts +++ b/codemods/find-dom-node/scripts/codemod.ts @@ -3,6 +3,12 @@ import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; import { getImport } from "@jssg/utils/javascript/imports"; +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + + const transform: Transform = async (root) => { const rootNode = root.root(); const edits: Edit[] = []; @@ -36,7 +42,7 @@ const transform: Transform = async (root) => { edits.push(call.replace(replacement)); findDOMNodeMetric.increment({ - file: root.filename(), + file: metricFile(root.filename()), }); } diff --git a/codemods/find-dom-node/tests/findDOMNode/expected.tsx b/codemods/find-dom-node/tests/findDOMNode/expected.tsx new file mode 100644 index 0000000..275ac90 --- /dev/null +++ b/codemods/find-dom-node/tests/findDOMNode/expected.tsx @@ -0,0 +1,34 @@ +'use strict'; + +var React = require('React'); + +var Composer = React.createClass({ + componentWillReceiveProps: function(nextProps) { + React.findDOMNode(this); + return foo(React.findDOMNode(this.refs.input)); + }, + + foo: function() { + var ref = 'foo'; + var element = this.refs[ref]; + var domNode = React.findDOMNode(element); + }, + + bar: function() { + var thing = this.refs.foo; + React.findDOMNode(thing); + }, + + foobar: function() { + passThisOn(React.findDOMNode(this.refs.main.refs.list)); + }, +}); + +var SomeDialog = React.createClass({ + render: function() { + call(this.refs.SomeThing); + return ( +
+ ); + }, +}); diff --git a/codemods/find-dom-node/tests/findDOMNode/input.tsx b/codemods/find-dom-node/tests/findDOMNode/input.tsx new file mode 100644 index 0000000..ef5bc1b --- /dev/null +++ b/codemods/find-dom-node/tests/findDOMNode/input.tsx @@ -0,0 +1,34 @@ +'use strict'; + +var React = require('React'); + +var Composer = React.createClass({ + componentWillReceiveProps: function(nextProps) { + this.getDOMNode(); + return foo(this.refs.input.getDOMNode()); + }, + + foo: function() { + var ref = 'foo'; + var element = this.refs[ref]; + var domNode = element.getDOMNode(); + }, + + bar: function() { + var thing = this.refs.foo; + thing.getDOMNode(); + }, + + foobar: function() { + passThisOn(this.refs.main.refs.list.getDOMNode()); + }, +}); + +var SomeDialog = React.createClass({ + render: function() { + call(this.refs.SomeThing); + return ( +
+ ); + }, +}); diff --git a/find-dom-node/tests/nested-refs/metrics.json b/codemods/find-dom-node/tests/findDOMNode/metrics.json similarity index 57% rename from find-dom-node/tests/nested-refs/metrics.json rename to codemods/find-dom-node/tests/findDOMNode/metrics.json index efe452d..960ef58 100644 --- a/find-dom-node/tests/nested-refs/metrics.json +++ b/codemods/find-dom-node/tests/findDOMNode/metrics.json @@ -2,9 +2,9 @@ "find-dom-node-replacements": [ { "cardinality": { - "file": "tests/nested-refs/input.tsx" + "file": "tests/findDOMNode/input.tsx" }, - "count": 1 + "count": 5 } ] } \ No newline at end of file diff --git a/pure-render-mixin/tsconfig.json b/codemods/find-dom-node/tsconfig.json similarity index 61% rename from pure-render-mixin/tsconfig.json rename to codemods/find-dom-node/tsconfig.json index 469fc5a..decec57 100644 --- a/pure-render-mixin/tsconfig.json +++ b/codemods/find-dom-node/tsconfig.json @@ -1,11 +1,19 @@ { "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], "module": "NodeNext", "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], + "types": [ + "@codemod.com/jssg-types", + "node" + ], "allowImportingTsExtensions": true, "noEmit": true, - "verbatimModuleSyntax": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, "erasableSyntaxOnly": true, "strict": true, "strictNullChecks": true, @@ -13,5 +21,7 @@ "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true }, - "exclude": ["tests"] + "exclude": [ + "tests" + ] } diff --git a/find-dom-node/workflow.yaml b/codemods/find-dom-node/workflow.yaml similarity index 100% rename from find-dom-node/workflow.yaml rename to codemods/find-dom-node/workflow.yaml diff --git a/codemods/manual-bind-to-arrow/README.md b/codemods/manual-bind-to-arrow/README.md new file mode 100644 index 0000000..9007c9f --- /dev/null +++ b/codemods/manual-bind-to-arrow/README.md @@ -0,0 +1,16 @@ +# @react/manual-bind-to-arrow + +Convert constructor-time `.bind(this)` assignments into arrow class fields. + +## Usage + +```bash +codemod run @react/manual-bind-to-arrow +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/manual-bind-to-arrow/codemod.yaml b/codemods/manual-bind-to-arrow/codemod.yaml similarity index 68% rename from manual-bind-to-arrow/codemod.yaml rename to codemods/manual-bind-to-arrow/codemod.yaml index bdd8cae..f0f0689 100644 --- a/manual-bind-to-arrow/codemod.yaml +++ b/codemods/manual-bind-to-arrow/codemod.yaml @@ -1,16 +1,16 @@ schema_version: "1.0" -name: "@react-codemods/manual-bind-to-arrow" +name: "@react/manual-bind-to-arrow" version: "0.1.0" description: "Convert this.method = this.method.bind(this) to arrow class property" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "bind", "arrow", "react"] +keywords: ["React"] registry: access: "public" diff --git a/manual-bind-to-arrow/package.json b/codemods/manual-bind-to-arrow/package.json similarity index 77% rename from manual-bind-to-arrow/package.json rename to codemods/manual-bind-to-arrow/package.json index 2d5daf1..c398f49 100644 --- a/manual-bind-to-arrow/package.json +++ b/codemods/manual-bind-to-arrow/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/manual-bind-to-arrow", + "name": "@react/manual-bind-to-arrow", "version": "0.1.0", "description": "Convert this.method = this.method.bind(this) to arrow class property", "type": "module", @@ -9,6 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" } } diff --git a/manual-bind-to-arrow/scripts/codemod.ts b/codemods/manual-bind-to-arrow/scripts/codemod.ts similarity index 58% rename from manual-bind-to-arrow/scripts/codemod.ts rename to codemods/manual-bind-to-arrow/scripts/codemod.ts index ded1d41..462b54d 100644 --- a/manual-bind-to-arrow/scripts/codemod.ts +++ b/codemods/manual-bind-to-arrow/scripts/codemod.ts @@ -2,6 +2,12 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + + function methodDefByNameRule(methodName: string) { const escaped = methodName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return { @@ -26,6 +32,29 @@ function getClassBody(node: SgNode): SgNode | null { return classDecl.find({ rule: { kind: "class_body" } }); } +function memberPropertyName(node: SgNode | null): string | null { + return node?.kind() === "member_expression" ? (node.field("property")?.text() ?? null) : null; +} + +function memberObject(node: SgNode | null): SgNode | null { + return node?.kind() === "member_expression" ? node.field("object") : null; +} + +function isThisLikeObject(node: SgNode | null): boolean { + if (!node) return false; + if (node.text() === "this" || node.text() === "self") return true; + return node.kind() === "parenthesized_expression" && node.find({ rule: { kind: "this" } }) !== null; +} + +function isSuperStatement(node: SgNode): boolean { + const call = node.find({ rule: { kind: "call_expression" } }); + return node.kind() === "expression_statement" && call?.field("function")?.text() === "super"; +} + +function statementForAssignment(assign: SgNode): SgNode | null { + return assign.ancestors().find((a) => a.kind() === "expression_statement") ?? null; +} + const transform: Transform = async (root) => { const rootNode = root.root(); const source = rootNode.text(); @@ -41,7 +70,7 @@ const transform: Transform = async (root) => { if (callee.field("property")?.text() !== "bind") return false; const left = assign.field("left"); if (!left || left.kind() !== "member_expression") return false; - if (left.field("object")?.text() !== "this") return false; + if (!isThisLikeObject(left.field("object"))) return false; return true; }); const assignmentsInConstructors = allBindAssignments.filter((assign) => { @@ -56,9 +85,10 @@ const transform: Transform = async (root) => { const bindCallee = right.field("function"); if (!bindCallee || bindCallee.kind() !== "member_expression") return false; const bindObj = bindCallee.field("object"); - const bindObjProp = bindObj?.kind() === "member_expression" ? bindObj.field("property") : null; - const leftProp = left.field("property"); - return leftProp && bindObjProp && leftProp.text() === bindObjProp.text(); + if (!bindObj || memberObject(bindObj)?.text() !== "this") return false; + const bindObjProp = memberPropertyName(bindObj); + const leftProp = memberPropertyName(left); + return !!leftProp && !!bindObjProp && leftProp === bindObjProp; }); for (const assign of assignments) { @@ -80,7 +110,6 @@ const transform: Transform = async (root) => { const stmt = assign.ancestors().find((a) => a.kind() === "expression_statement"); const constructorNode = assign.ancestors().find((a) => a.kind() === "method_definition" && (a.field("name") ?? a.field("key"))?.text() === "constructor"); const constructorBody = constructorNode?.field("body"); - const stmtCount = constructorBody?.children().filter((c) => c.kind() === "expression_statement" || c.kind() === "lexical_declaration" || c.kind() === "variable_declaration").length ?? 0; const bindAssignmentsInConstructor = constructorNode ? assignments .filter((a) => { @@ -89,22 +118,28 @@ const transform: Transform = async (root) => { }) .sort((a, b) => a.range().start.index - b.range().start.index) : []; - const isLastBindInConstructor = - bindAssignmentsInConstructor.length > 0 && - bindAssignmentsInConstructor[bindAssignmentsInConstructor.length - 1]!.range().start.index === assign.range().start.index; + const selfIdentifiers = constructorNode?.findAll({ rule: { kind: "identifier", regex: "^self$" } }) ?? []; + const selfDeclaration = constructorNode?.findAll({ rule: { kind: "lexical_declaration" } }).find((decl) => + decl.find({ rule: { kind: "variable_declarator", has: { field: "name", kind: "identifier", regex: "^self$" } } }) !== null + ) ?? null; + const canRemoveSelfDeclaration = + left?.field("object")?.text() === "self" && selfIdentifiers.length <= 2 && !!selfDeclaration; + const removableBindStatements = new Set( + bindAssignmentsInConstructor + .map(statementForAssignment) + .filter((node): node is SgNode => node !== null) + .map((node) => node.range().start.index), + ); const willBeEmptyConstructor = - stmtCount === 1 + bindAssignmentsInConstructor.length && isLastBindInConstructor && !!constructorNode; - - if (stmt) { - const stmtEnd = stmt.range().end.index; - const after = source.slice(stmtEnd, stmtEnd + 10); - const trailing = after.match(/^\s*\n?/)?.[0] ?? "\n"; - edits.push({ - startPos: stmt.range().start.index, - endPos: stmtEnd + trailing.length, - insertedText: "", - }); - } + !!constructorNode && + !!constructorBody && + constructorBody.children() + .filter((c) => c.isNamed()) + .every((child) => + isSuperStatement(child) || + removableBindStatements.has(child.range().start.index) || + (canRemoveSelfDeclaration && selfDeclaration?.range().start.index === child.range().start.index) + ); if (willBeEmptyConstructor && constructorNode) { const ctorEnd = constructorNode.range().end.index; @@ -115,14 +150,35 @@ const transform: Transform = async (root) => { endPos: ctorEnd + ctorTrailing.length, insertedText: "", }); + } else if (stmt) { + const stmtEnd = stmt.range().end.index; + const after = source.slice(stmtEnd, stmtEnd + 10); + const trailing = after.match(/^\s*\n?/)?.[0] ?? "\n"; + edits.push({ + startPos: stmt.range().start.index, + endPos: stmtEnd + trailing.length, + insertedText: "", + }); + + if (canRemoveSelfDeclaration && selfDeclaration) { + const selfEnd = selfDeclaration.range().end.index; + const afterSelf = source.slice(selfEnd, selfEnd + 10); + const selfTrailing = afterSelf.match(/^\s*\n?/)?.[0] ?? "\n"; + edits.push({ + startPos: selfDeclaration.range().start.index, + endPos: selfEnd + selfTrailing.length, + insertedText: "", + }); + } } const paramsText = params ? params.text() : "()"; + const returnTypeText = methodDef.field("return_type")?.text() ?? ""; const bodyText = body.text(); - const arrowReplacement = `${methodName} = ${paramsText} => ${bodyText}`; + const arrowReplacement = `${methodName} = ${paramsText}${returnTypeText} => ${bodyText};`; edits.push(methodDef.replace(arrowReplacement)); - metric.increment({ file: root.filename() }); + metric.increment({ file: metricFile(root.filename()) }); } if (edits.length === 0) return null; diff --git a/manual-bind-to-arrow/tests/simple-bind/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/expected.tsx similarity index 65% rename from manual-bind-to-arrow/tests/simple-bind/expected.tsx rename to codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/expected.tsx index 168d91d..6f9abaf 100644 --- a/manual-bind-to-arrow/tests/simple-bind/expected.tsx +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/expected.tsx @@ -1,3 +1,3 @@ class Component extends React.Component { - onClick = () => { } + onClick = () => { }; } diff --git a/manual-bind-to-arrow/tests/simple-bind/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/input.tsx similarity index 100% rename from manual-bind-to-arrow/tests/simple-bind/input.tsx rename to codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/input.tsx diff --git a/manual-bind-to-arrow/tests/simple-bind/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/metrics.json similarity index 65% rename from manual-bind-to-arrow/tests/simple-bind/metrics.json rename to codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/metrics.json index 00745db..4de0031 100644 --- a/manual-bind-to-arrow/tests/simple-bind/metrics.json +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/metrics.json @@ -2,7 +2,7 @@ "manual-bind-to-arrow-transforms": [ { "cardinality": { - "file": "tests/simple-bind/input.tsx" + "file": "tests/manual-bind-to-arrow1/input.tsx" }, "count": 1 } diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/expected.tsx new file mode 100644 index 0000000..1113e05 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/expected.tsx @@ -0,0 +1,7 @@ +class Component extends React.Component { + onClick = () => { }; +} + +class Component2 extends React.Component { + onClick = () => { }; +} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/input.tsx new file mode 100644 index 0000000..d01be8a --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/input.tsx @@ -0,0 +1,15 @@ +class Component extends React.Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + } + onClick() { } +} + +class Component2 extends React.Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + } + onClick() { } +} \ No newline at end of file diff --git a/manual-bind-to-arrow/tests/real-world-dropdown/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/metrics.json similarity index 65% rename from manual-bind-to-arrow/tests/real-world-dropdown/metrics.json rename to codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/metrics.json index 72328ee..6ce9633 100644 --- a/manual-bind-to-arrow/tests/real-world-dropdown/metrics.json +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/metrics.json @@ -2,7 +2,7 @@ "manual-bind-to-arrow-transforms": [ { "cardinality": { - "file": "tests/real-world-dropdown/input.tsx" + "file": "tests/manual-bind-to-arrow10/input.tsx" }, "count": 2 } diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/expected.tsx new file mode 100644 index 0000000..ddcc601 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/expected.tsx @@ -0,0 +1,12 @@ +class Component extends React.Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + } + + onClick() { + if (fn) { + fn.apply(this, Array.prototype.slice.call(arguments, 1)) + } + } +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/input.tsx new file mode 100644 index 0000000..ddcc601 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/input.tsx @@ -0,0 +1,12 @@ +class Component extends React.Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + } + + onClick() { + if (fn) { + fn.apply(this, Array.prototype.slice.call(arguments, 1)) + } + } +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/expected.tsx new file mode 100644 index 0000000..e56e9c9 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/expected.tsx @@ -0,0 +1,18 @@ +class SomeName extends React.Component { + _onChange: Function; + + constructor(props: Object) { + super(props); + if (!this.props.something.somePreference) { + this._onChange(SomeConstantsOptions.SOME_CONSTANT); + } + } + + /* Some comment */ + _onChange = (value: String): void => { + Dispatcher.handleViewAction({ + type: SomeConstantsOptions.SOME_CONSTANT, + payload: value, + }); + }; +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/input.tsx new file mode 100644 index 0000000..434b3bc --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/input.tsx @@ -0,0 +1,19 @@ +class SomeName extends React.Component { + _onChange: Function; + + constructor(props: Object) { + super(props); + this._onChange = this._onChange.bind(this); + if (!this.props.something.somePreference) { + this._onChange(SomeConstantsOptions.SOME_CONSTANT); + } + } + + /* Some comment */ + _onChange(value: String): void { + Dispatcher.handleViewAction({ + type: SomeConstantsOptions.SOME_CONSTANT, + payload: value, + }); + } +} diff --git a/manual-bind-to-arrow/tests/constructor-with-props/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/metrics.json similarity index 65% rename from manual-bind-to-arrow/tests/constructor-with-props/metrics.json rename to codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/metrics.json index 208f39d..a426ece 100644 --- a/manual-bind-to-arrow/tests/constructor-with-props/metrics.json +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/metrics.json @@ -2,7 +2,7 @@ "manual-bind-to-arrow-transforms": [ { "cardinality": { - "file": "tests/constructor-with-props/input.tsx" + "file": "tests/manual-bind-to-arrow2/input.tsx" }, "count": 1 } diff --git a/manual-bind-to-arrow/tests/bind-no-matching-method/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/expected.tsx similarity index 100% rename from manual-bind-to-arrow/tests/bind-no-matching-method/expected.tsx rename to codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/expected.tsx diff --git a/manual-bind-to-arrow/tests/bind-no-matching-method/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/input.tsx similarity index 100% rename from manual-bind-to-arrow/tests/bind-no-matching-method/input.tsx rename to codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/input.tsx diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/expected.tsx new file mode 100644 index 0000000..6f9abaf --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/expected.tsx @@ -0,0 +1,3 @@ +class Component extends React.Component { + onClick = () => { }; +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/input.tsx new file mode 100644 index 0000000..65d0ef6 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/input.tsx @@ -0,0 +1,7 @@ +class Component extends React.Component { + constructor() { + super(); + (this: any).onClick = this.onClick.bind(this); + } + onClick() { } +} diff --git a/manual-bind-to-arrow/tests/bind-with-other-constructor-code/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/metrics.json similarity index 61% rename from manual-bind-to-arrow/tests/bind-with-other-constructor-code/metrics.json rename to codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/metrics.json index c919dca..574360f 100644 --- a/manual-bind-to-arrow/tests/bind-with-other-constructor-code/metrics.json +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/metrics.json @@ -2,7 +2,7 @@ "manual-bind-to-arrow-transforms": [ { "cardinality": { - "file": "tests/bind-with-other-constructor-code/input.tsx" + "file": "tests/manual-bind-to-arrow4/input.tsx" }, "count": 1 } diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/expected.tsx new file mode 100644 index 0000000..6f9abaf --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/expected.tsx @@ -0,0 +1,3 @@ +class Component extends React.Component { + onClick = () => { }; +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/input.tsx new file mode 100644 index 0000000..34e18c2 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/input.tsx @@ -0,0 +1,8 @@ +class Component extends React.Component { + constructor() { + super(); + const self: any = this; + self.onClick = this.onClick.bind(this); + } + onClick() { } +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/metrics.json new file mode 100644 index 0000000..3f87734 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/metrics.json @@ -0,0 +1,10 @@ +{ + "manual-bind-to-arrow-transforms": [ + { + "cardinality": { + "file": "tests/manual-bind-to-arrow5/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/expected.tsx new file mode 100644 index 0000000..3ce6360 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/expected.tsx @@ -0,0 +1,9 @@ +class Component extends React.Component { + constructor() { + super(); + const self: any = this; + self._onMapResize = debounceCore(this._onMapResize.bind(this), 100); + } + onClick = () => { }; + onMapResize() { } +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/input.tsx new file mode 100644 index 0000000..44c5135 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/input.tsx @@ -0,0 +1,10 @@ +class Component extends React.Component { + constructor() { + super(); + const self: any = this; + self._onMapResize = debounceCore(this._onMapResize.bind(this), 100); + self.onClick = this.onClick.bind(this); + } + onClick() { } + onMapResize() { } +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/metrics.json new file mode 100644 index 0000000..22e736c --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/metrics.json @@ -0,0 +1,10 @@ +{ + "manual-bind-to-arrow-transforms": [ + { + "cardinality": { + "file": "tests/manual-bind-to-arrow6/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/expected.tsx new file mode 100644 index 0000000..8f8e7fa --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/expected.tsx @@ -0,0 +1,9 @@ +class SomeClass { + constructor() { + (this: any)._isLoaded = false; + } + + isLoaded = (): boolean => { + return this._isLoaded; + }; +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/input.tsx new file mode 100644 index 0000000..44af15d --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/input.tsx @@ -0,0 +1,10 @@ +class SomeClass { + constructor() { + (this: any)._isLoaded = false; + (this: any).isLoaded = this.isLoaded.bind(this); + } + + isLoaded(): boolean { + return this._isLoaded; + } +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/metrics.json new file mode 100644 index 0000000..475fc7f --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/metrics.json @@ -0,0 +1,10 @@ +{ + "manual-bind-to-arrow-transforms": [ + { + "cardinality": { + "file": "tests/manual-bind-to-arrow7/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/expected.tsx new file mode 100644 index 0000000..6f9abaf --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/expected.tsx @@ -0,0 +1,3 @@ +class Component extends React.Component { + onClick = () => { }; +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/input.tsx new file mode 100644 index 0000000..d72504b --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/input.tsx @@ -0,0 +1,7 @@ +class Component extends React.Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + } + async onClick() { } +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/metrics.json new file mode 100644 index 0000000..b283aae --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/metrics.json @@ -0,0 +1,10 @@ +{ + "manual-bind-to-arrow-transforms": [ + { + "cardinality": { + "file": "tests/manual-bind-to-arrow8/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/expected.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/expected.tsx new file mode 100644 index 0000000..19c2ed0 --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/expected.tsx @@ -0,0 +1,7 @@ +class Component extends React.Component { + onClick = () => { }; +} + +class Component2 extends React.Component { + didClick = () => { }; +} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/input.tsx new file mode 100644 index 0000000..3122cab --- /dev/null +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/input.tsx @@ -0,0 +1,15 @@ +class Component extends React.Component { + constructor() { + super(); + this.onClick = this.onClick.bind(this); + } + onClick() { } +} + +class Component2 extends React.Component { + constructor() { + super(); + this.didClick = this.didClick.bind(this); + } + didClick() { } +} \ No newline at end of file diff --git a/manual-bind-to-arrow/tests/real-world-todo-item/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/metrics.json similarity index 65% rename from manual-bind-to-arrow/tests/real-world-todo-item/metrics.json rename to codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/metrics.json index 3e73cb1..a615d80 100644 --- a/manual-bind-to-arrow/tests/real-world-todo-item/metrics.json +++ b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/metrics.json @@ -2,7 +2,7 @@ "manual-bind-to-arrow-transforms": [ { "cardinality": { - "file": "tests/real-world-todo-item/input.tsx" + "file": "tests/manual-bind-to-arrow9/input.tsx" }, "count": 2 } diff --git a/codemods/manual-bind-to-arrow/tsconfig.json b/codemods/manual-bind-to-arrow/tsconfig.json new file mode 100644 index 0000000..ecbb40a --- /dev/null +++ b/codemods/manual-bind-to-arrow/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "strict": true + }, + "exclude": [ + "tests" + ] +} diff --git a/manual-bind-to-arrow/workflow.yaml b/codemods/manual-bind-to-arrow/workflow.yaml similarity index 100% rename from manual-bind-to-arrow/workflow.yaml rename to codemods/manual-bind-to-arrow/workflow.yaml diff --git a/codemods/pure-render-mixin/README.md b/codemods/pure-render-mixin/README.md new file mode 100644 index 0000000..be9571a --- /dev/null +++ b/codemods/pure-render-mixin/README.md @@ -0,0 +1,20 @@ +# @react/pure-render-mixin + +Replace `PureRenderMixin` with `shouldComponentUpdate` using `React.addons.shallowCompare`. + +## Usage + +```bash +codemod run @react/pure-render-mixin +``` + +## Options + +- `mixin-name`: custom mixin identifier to replace. Default: `PureRenderMixin`. + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/pure-render-mixin/codemod.yaml b/codemods/pure-render-mixin/codemod.yaml similarity index 64% rename from pure-render-mixin/codemod.yaml rename to codemods/pure-render-mixin/codemod.yaml index 9ef2cd5..554d2ef 100644 --- a/pure-render-mixin/codemod.yaml +++ b/codemods/pure-render-mixin/codemod.yaml @@ -1,16 +1,16 @@ schema_version: "1.0" -name: "@react-codemods/pure-render-mixin" +name: "@react/pure-render-mixin" version: "0.1.0" description: "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration", "pure-render-mixin", "shouldComponentUpdate"] +keywords: ["React"] registry: access: "public" diff --git a/pure-render-mixin/package.json b/codemods/pure-render-mixin/package.json similarity index 78% rename from pure-render-mixin/package.json rename to codemods/pure-render-mixin/package.json index 558ee4b..83daaf7 100644 --- a/pure-render-mixin/package.json +++ b/codemods/pure-render-mixin/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/pure-render-mixin", + "name": "@react/pure-render-mixin", "version": "0.1.0", "description": "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare", "type": "module", @@ -9,6 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" } } diff --git a/pure-render-mixin/scripts/codemod.ts b/codemods/pure-render-mixin/scripts/codemod.ts similarity index 80% rename from pure-render-mixin/scripts/codemod.ts rename to codemods/pure-render-mixin/scripts/codemod.ts index 717baf0..0598203 100644 --- a/pure-render-mixin/scripts/codemod.ts +++ b/codemods/pure-render-mixin/scripts/codemod.ts @@ -2,6 +2,12 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + + const DEFAULT_MIXIN_NAME = "PureRenderMixin"; const SHOULD_COMPONENT_UPDATE = "shouldComponentUpdate"; const RENDER = "render"; @@ -33,6 +39,10 @@ function arrayIdentifierNamedRule(name: string) { return { rule: { kind: "identifier" as const, regex: `^${escaped}$` } }; } +function getPairKeyName(node: SgNode): string { + return node.field("key")?.text() ?? node.field("name")?.text() ?? ""; +} + const transform: Transform = async (root, options) => { const rootNode = root.root(); const source = rootNode.text(); @@ -111,19 +121,24 @@ const transform: Transform = async (root, options) => { edits.push(mixinsPair.replace(newMixinsText)); } - const renderStart = renderPair.range().start.index; - const beforeRender = source.slice(Math.max(0, renderStart - 30), renderStart); - const indentMatch = beforeRender.match(/\n(\s*)$/); + const allPairsByPosition = [...allPairs].sort((a, b) => a.range().start.index - b.range().start.index); + const lastPair = allPairsByPosition[allPairsByPosition.length - 1]!; + const insertBeforeRender = getPairKeyName(lastPair) === RENDER; + const insertPos = insertBeforeRender ? renderPair.range().start.index : lastPair.range().end.index; + const beforeInsert = source.slice(Math.max(0, insertPos - 30), insertPos); + const indentMatch = beforeInsert.match(/\n(\s*)$/); const indent = indentMatch ? indentMatch[1] : " "; - const shouldUpdateBlock = `${indent}${SHOULD_COMPONENT_UPDATE}: function(nextProps, nextState) {\n${indent} return React.addons.shallowCompare(this, nextProps, nextState);\n${indent}},\n${indent}`; + const shouldUpdateBlock = insertBeforeRender + ? `${indent}${SHOULD_COMPONENT_UPDATE}: function(nextProps, nextState) {\n${indent} return React.addons.shallowCompare(this, nextProps, nextState);\n${indent}},\n${indent}` + : `,\n\n${indent}${SHOULD_COMPONENT_UPDATE}: function(nextProps, nextState) {\n${indent} return React.addons.shallowCompare(this, nextProps, nextState);\n${indent}}`; edits.push({ - startPos: renderStart, - endPos: renderStart, + startPos: insertPos, + endPos: insertPos, insertedText: shouldUpdateBlock, }); transformCount++; - metric.increment({ file: root.filename() }); + metric.increment({ file: metricFile(root.filename()) }); } if (edits.length > 0 && transformCount > 0) { diff --git a/pure-render-mixin/tests/original-mixed-mixins/expected.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin/expected.tsx similarity index 99% rename from pure-render-mixin/tests/original-mixed-mixins/expected.tsx rename to codemods/pure-render-mixin/tests/pure-render-mixin/expected.tsx index a608307..a6a8ded 100644 --- a/pure-render-mixin/tests/original-mixed-mixins/expected.tsx +++ b/codemods/pure-render-mixin/tests/pure-render-mixin/expected.tsx @@ -33,13 +33,6 @@ var MyMixedComponent = React.createClass({ var MyFooComponent = React.createClass({ mixins: [SomeOtherMixin], - - shouldComponentUpdate: function(nextProps, nextState) { - - return React.addons.shallowCompare(this, nextProps, nextState); - - }, - render: function() { return
; }, @@ -47,6 +40,10 @@ var MyFooComponent = React.createClass({ foo: function() { }, + + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + }, }); var MyStupidComponent = React.createClass({ diff --git a/pure-render-mixin/tests/original-mixed-mixins/input.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin/input.tsx similarity index 100% rename from pure-render-mixin/tests/original-mixed-mixins/input.tsx rename to codemods/pure-render-mixin/tests/pure-render-mixin/input.tsx diff --git a/pure-render-mixin/tests/original-mixed-mixins/metrics.json b/codemods/pure-render-mixin/tests/pure-render-mixin/metrics.json similarity index 65% rename from pure-render-mixin/tests/original-mixed-mixins/metrics.json rename to codemods/pure-render-mixin/tests/pure-render-mixin/metrics.json index cb8ca5d..9abd990 100644 --- a/pure-render-mixin/tests/original-mixed-mixins/metrics.json +++ b/codemods/pure-render-mixin/tests/pure-render-mixin/metrics.json @@ -2,7 +2,7 @@ "pure-render-mixin-replacements": [ { "cardinality": { - "file": "tests/original-mixed-mixins/input.tsx" + "file": "tests/pure-render-mixin/input.tsx" }, "count": 3 } diff --git a/pure-render-mixin/tests/original-single/expected.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin2/expected.tsx similarity index 100% rename from pure-render-mixin/tests/original-single/expected.tsx rename to codemods/pure-render-mixin/tests/pure-render-mixin2/expected.tsx diff --git a/pure-render-mixin/tests/original-single/input.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin2/input.tsx similarity index 100% rename from pure-render-mixin/tests/original-single/input.tsx rename to codemods/pure-render-mixin/tests/pure-render-mixin2/input.tsx diff --git a/pure-render-mixin/tests/real-world-form/metrics.json b/codemods/pure-render-mixin/tests/pure-render-mixin2/metrics.json similarity index 66% rename from pure-render-mixin/tests/real-world-form/metrics.json rename to codemods/pure-render-mixin/tests/pure-render-mixin2/metrics.json index b885487..40a2653 100644 --- a/pure-render-mixin/tests/real-world-form/metrics.json +++ b/codemods/pure-render-mixin/tests/pure-render-mixin2/metrics.json @@ -2,7 +2,7 @@ "pure-render-mixin-replacements": [ { "cardinality": { - "file": "tests/real-world-form/input.tsx" + "file": "tests/pure-render-mixin2/input.tsx" }, "count": 1 } diff --git a/pure-render-mixin/tests/original-fixture3-removes-var/expected.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin3/expected.tsx similarity index 100% rename from pure-render-mixin/tests/original-fixture3-removes-var/expected.tsx rename to codemods/pure-render-mixin/tests/pure-render-mixin3/expected.tsx diff --git a/pure-render-mixin/tests/original-fixture3-removes-var/input.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin3/input.tsx similarity index 100% rename from pure-render-mixin/tests/original-fixture3-removes-var/input.tsx rename to codemods/pure-render-mixin/tests/pure-render-mixin3/input.tsx diff --git a/pure-render-mixin/tests/original-single/metrics.json b/codemods/pure-render-mixin/tests/pure-render-mixin3/metrics.json similarity index 66% rename from pure-render-mixin/tests/original-single/metrics.json rename to codemods/pure-render-mixin/tests/pure-render-mixin3/metrics.json index 2014f7e..90c32c0 100644 --- a/pure-render-mixin/tests/original-single/metrics.json +++ b/codemods/pure-render-mixin/tests/pure-render-mixin3/metrics.json @@ -2,7 +2,7 @@ "pure-render-mixin-replacements": [ { "cardinality": { - "file": "tests/original-single/input.tsx" + "file": "tests/pure-render-mixin3/input.tsx" }, "count": 1 } diff --git a/pure-render-mixin/tests/create-class-direct-call/expected.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin4/expected.tsx similarity index 50% rename from pure-render-mixin/tests/create-class-direct-call/expected.tsx rename to codemods/pure-render-mixin/tests/pure-render-mixin4/expected.tsx index c913438..71bcd04 100644 --- a/pure-render-mixin/tests/create-class-direct-call/expected.tsx +++ b/codemods/pure-render-mixin/tests/pure-render-mixin4/expected.tsx @@ -1,7 +1,5 @@ -var React = require('react/addons'); -var createClass = React.createClass; - -var Sidebar = createClass({ +var React = require('React'); +var MyComponent = React.createClass({ shouldComponentUpdate: function(nextProps, nextState) { @@ -10,8 +8,8 @@ var Sidebar = createClass({ }, render: function() { - return ; + return
; }, }); -module.exports = Sidebar; +module.exports = MyComponent; diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin4/input.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin4/input.tsx new file mode 100644 index 0000000..2fe0945 --- /dev/null +++ b/codemods/pure-render-mixin/tests/pure-render-mixin4/input.tsx @@ -0,0 +1,12 @@ +var React = require('React'); +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); + +var MyComponent = React.createClass({ + mixins: [ReactComponentWithPureRenderMixin], + + render: function() { + return
; + }, +}); + +module.exports = MyComponent; diff --git a/pure-render-mixin/tests/real-world-list-item/metrics.json b/codemods/pure-render-mixin/tests/pure-render-mixin4/metrics.json similarity index 65% rename from pure-render-mixin/tests/real-world-list-item/metrics.json rename to codemods/pure-render-mixin/tests/pure-render-mixin4/metrics.json index 3a33f3f..2f39039 100644 --- a/pure-render-mixin/tests/real-world-list-item/metrics.json +++ b/codemods/pure-render-mixin/tests/pure-render-mixin4/metrics.json @@ -2,7 +2,7 @@ "pure-render-mixin-replacements": [ { "cardinality": { - "file": "tests/real-world-list-item/input.tsx" + "file": "tests/pure-render-mixin4/input.tsx" }, "count": 1 } diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin4/test.config.json b/codemods/pure-render-mixin/tests/pure-render-mixin4/test.config.json new file mode 100644 index 0000000..b9f5c63 --- /dev/null +++ b/codemods/pure-render-mixin/tests/pure-render-mixin4/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "mixin-name": "ReactComponentWithPureRenderMixin" + } +} diff --git a/react-proptypes-to-prop-types/tsconfig.json b/codemods/pure-render-mixin/tsconfig.json similarity index 61% rename from react-proptypes-to-prop-types/tsconfig.json rename to codemods/pure-render-mixin/tsconfig.json index 469fc5a..decec57 100644 --- a/react-proptypes-to-prop-types/tsconfig.json +++ b/codemods/pure-render-mixin/tsconfig.json @@ -1,11 +1,19 @@ { "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], "module": "NodeNext", "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], + "types": [ + "@codemod.com/jssg-types", + "node" + ], "allowImportingTsExtensions": true, "noEmit": true, - "verbatimModuleSyntax": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, "erasableSyntaxOnly": true, "strict": true, "strictNullChecks": true, @@ -13,5 +21,7 @@ "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true }, - "exclude": ["tests"] + "exclude": [ + "tests" + ] } diff --git a/replace-string-ref/workflow.yaml b/codemods/pure-render-mixin/workflow.yaml similarity index 66% rename from replace-string-ref/workflow.yaml rename to codemods/pure-render-mixin/workflow.yaml index 8773841..0f8367a 100644 --- a/replace-string-ref/workflow.yaml +++ b/codemods/pure-render-mixin/workflow.yaml @@ -2,6 +2,14 @@ version: "1" +params: + schema: + mixin-name: + name: "Mixin Name" + description: "Identifier name for the pure render mixin to replace." + type: string + default: "PureRenderMixin" + nodes: - id: apply-transforms name: Apply AST Transformations diff --git a/codemods/react-proptypes-to-prop-types/README.md b/codemods/react-proptypes-to-prop-types/README.md new file mode 100644 index 0000000..dc92c06 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/README.md @@ -0,0 +1,20 @@ +# @react/react-proptypes-to-prop-types + +Replace legacy React `PropTypes` usage with the `prop-types` package across imports, requires, aliases, and destructuring patterns. + +## Usage + +```bash +codemod run @react/react-proptypes-to-prop-types +``` + +## Options + +- `module-name`: module specifier to import or require for `PropTypes`. Default: `prop-types`. + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/react-proptypes-to-prop-types/codemod.yaml b/codemods/react-proptypes-to-prop-types/codemod.yaml similarity index 64% rename from react-proptypes-to-prop-types/codemod.yaml rename to codemods/react-proptypes-to-prop-types/codemod.yaml index fc93ba1..e68b5c5 100644 --- a/react-proptypes-to-prop-types/codemod.yaml +++ b/codemods/react-proptypes-to-prop-types/codemod.yaml @@ -1,16 +1,16 @@ schema_version: "1.0" -name: "@react-codemods/react-proptypes-to-prop-types" +name: "@react/react-proptypes-to-prop-types" version: "0.1.0" description: "Replace React.PropTypes with prop-types package" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration", "prop-types"] +keywords: ["React"] registry: access: "public" diff --git a/react-proptypes-to-prop-types/package.json b/codemods/react-proptypes-to-prop-types/package.json similarity index 77% rename from react-proptypes-to-prop-types/package.json rename to codemods/react-proptypes-to-prop-types/package.json index 2bfac08..51a88ef 100644 --- a/react-proptypes-to-prop-types/package.json +++ b/codemods/react-proptypes-to-prop-types/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/react-proptypes-to-prop-types", + "name": "@react/react-proptypes-to-prop-types", "version": "0.1.0", "description": "Replace React.PropTypes with prop-types package", "type": "module", @@ -9,7 +9,8 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" }, "dependencies": { "@jssg/utils": "^0.0.2" diff --git a/codemods/react-proptypes-to-prop-types/scripts/codemod.ts b/codemods/react-proptypes-to-prop-types/scripts/codemod.ts new file mode 100644 index 0000000..6a91929 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/scripts/codemod.ts @@ -0,0 +1,337 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + +function isWhitespace(char: string | undefined): boolean { + return char === " " || char === "\t" || char === "\n" || char === "\r"; +} + +function sourceText(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = node.text(); + if (text.length >= 2) return text.slice(1, -1); + return null; +} + +function statementRange(node: SgNode, source: string): { start: number; end: number } { + const range = node.range(); + let end = range.end.index; + while (end < source.length && (source[end] === "\n" || source[end] === "\r")) end++; + return { start: range.start.index, end }; +} + +function statementRangeWithLeadingLineComments( + node: SgNode, + source: string, +): { start: number; end: number } { + const range = statementRange(node, source); + let start = range.start; + + while (start > 0) { + const lineEnd = start; + let lineStart = source.lastIndexOf("\n", lineEnd - 2); + lineStart = lineStart === -1 ? 0 : lineStart + 1; + const line = source.slice(lineStart, lineEnd).trim(); + if (line === "" || line.startsWith("//")) { + start = lineStart; + continue; + } + break; + } + + return { start, end: range.end }; +} + +function removeNodeWithComma(node: SgNode, source: string): Edit { + let start = node.range().start.index; + let end = node.range().end.index; + + let i = end; + while (isWhitespace(source[i])) i++; + if (source[i] === ",") { + end = i + 1; + while (isWhitespace(source[end])) end++; + } else { + let j = start - 1; + while (isWhitespace(source[j])) j--; + if (source[j] === ",") start = j; + } + + return { startPos: start, endPos: end, insertedText: "" }; +} + +function nearestStatement(node: SgNode): SgNode | null { + return node.ancestors().find((a) => + a.kind() === "lexical_declaration" || + a.kind() === "variable_declaration" || + a.kind() === "import_statement" + ) ?? null; +} + +function isRequireCall(node: SgNode | null, moduleNames: Set): boolean { + if (!node || node.kind() !== "call_expression") return false; + if (node.field("function")?.text() !== "require") return false; + const arg = node.field("arguments")?.find({ rule: { kind: "string" } }); + const source = arg ? sourceText(arg) : null; + return !!source && moduleNames.has(source); +} + +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? sourceText(source) : null; +} + +function hasImportOrRequire(rootNode: SgNode, moduleName: string): boolean { + const modules = new Set([moduleName]); + return rootNode.findAll({ rule: { kind: "import_statement" } }) + .some((node) => importSource(node) === moduleName) || + rootNode.findAll({ rule: { kind: "call_expression" } }) + .some((node) => isRequireCall(node, modules)); +} + +function usesImportSyntax(rootNode: SgNode): boolean { + return rootNode.findAll({ rule: { kind: "import_statement" } }) + .some((node) => !node.text().startsWith("import type")); +} + +function usesVarForRequires(rootNode: SgNode): boolean { + return rootNode.find({ rule: { kind: "lexical_declaration" } }) === null; +} + +function firstSortedImportPosition(rootNode: SgNode, moduleName: string): number { + const imports = rootNode.findAll({ rule: { kind: "import_statement" } }); + const lowerModule = moduleName.toLowerCase(); + let target: SgNode | null = null; + let targetName = ""; + + for (const imp of imports) { + const source = importSource(imp); + if (!source) continue; + const lowerSource = source.toLowerCase(); + if (lowerSource > lowerModule && (!target || lowerSource < targetName)) { + target = imp; + targetName = lowerSource; + } + } + + if (target) return target.range().start.index; + const last = imports[imports.length - 1]; + return last ? last.range().end.index : 0; +} + +function firstSortedRequirePosition(rootNode: SgNode, moduleName: string): number { + const lowerModule = moduleName.toLowerCase(); + let target: SgNode | null = null; + let targetName = ""; + + for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { + if (call.field("function")?.text() !== "require") continue; + const arg = call.field("arguments")?.find({ rule: { kind: "string" } }); + const source = arg ? sourceText(arg) : null; + if (!source) continue; + const lowerSource = source.toLowerCase(); + if (lowerSource > lowerModule && (!target || lowerSource < targetName)) { + const stmt = nearestStatement(call); + if (stmt) { + target = stmt; + targetName = lowerSource; + } + } + } + + if (target) return target.range().start.index; + return 0; +} + +function propTypesBindingFromModule(rootNode: SgNode, moduleName: string): string | null { + for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (importSource(imp) !== moduleName) continue; + const clause = imp.find({ rule: { kind: "import_clause" } }); + const defaultName = clause?.children().find((c) => c.kind() === "identifier"); + if (defaultName) return defaultName.text(); + const named = imp.findAll({ rule: { kind: "import_specifier" } }) + .find((spec) => spec.field("name")?.text() === "PropTypes"); + if (named && !named.field("alias")) return "PropTypes"; + } + + for (const decl of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { + const value = decl.field("value"); + if (!isRequireCall(value, new Set([moduleName]))) continue; + const name = decl.field("name"); + if (name?.kind() === "identifier") return name.text(); + } + + return null; +} + +function identifierNamesInPattern(pattern: SgNode): string[] { + return pattern.children() + .filter((child) => + child.kind() === "shorthand_property_identifier_pattern" || + child.kind() === "pair_pattern" + ) + .map((child) => { + const alias = child.field("value"); + if (alias?.kind() === "identifier") return alias.text(); + return child.field("key")?.text() ?? child.text(); + }); +} + +function removeNamedImportsAfterDefault(namedImports: SgNode, source: string): Edit { + let start = namedImports.range().start.index; + let i = start - 1; + while (isWhitespace(source[i])) i--; + if (source[i] === ",") start = i; + return { startPos: start, endPos: namedImports.range().end.index, insertedText: "" }; +} + +const transform: Transform = async (root, options) => { + const rootNode = root.root(); + const source = rootNode.text(); + const edits: Edit[] = []; + const metric = useMetricAtom("react-proptypes-migrations"); + const moduleName = (options.params?.["module-name"] as string | undefined) ?? "prop-types"; + const reactModules = new Set(["react", "React"]); + const reactNames = new Set(["React"]); + let localPropTypesName = propTypesBindingFromModule(rootNode, moduleName) ?? "PropTypes"; + let addedPropTypesImportByReplacement = false; + let changed = false; + + for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { + const sourceName = importSource(imp); + if (sourceName && reactModules.has(sourceName)) { + const clause = imp.find({ rule: { kind: "import_clause" } }); + const defaultName = clause?.children().find((c) => c.kind() === "identifier"); + if (defaultName) reactNames.add(defaultName.text()); + } + } + + for (const decl of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { + const value = decl.field("value"); + const name = decl.field("name"); + if (name?.kind() === "identifier" && isRequireCall(value, reactModules)) { + reactNames.add(name.text()); + } + } + + for (const spec of rootNode.findAll({ rule: { kind: "import_specifier" } })) { + const parentImport = spec.ancestors().find((a) => a.kind() === "import_statement"); + if (!parentImport || !reactModules.has(importSource(parentImport) ?? "")) continue; + if (spec.field("name")?.text() !== "PropTypes") continue; + localPropTypesName = spec.field("alias")?.text() ?? "PropTypes"; + const namedImports = spec.ancestors().find((a) => a.kind() === "named_imports"); + const specifierCount = namedImports?.findAll({ rule: { kind: "import_specifier" } }).length ?? 0; + const importClause = parentImport.find({ rule: { kind: "import_clause" } }); + const hasDefault = importClause?.children().some((c) => c.kind() === "identifier") ?? false; + if (specifierCount === 1 && !hasDefault) { + if (!hasImportOrRequire(rootNode, moduleName)) { + edits.push(parentImport.replace(`import ${localPropTypesName} from '${moduleName}';`)); + addedPropTypesImportByReplacement = true; + } else { + const { start, end } = statementRange(parentImport, source); + edits.push({ startPos: start, endPos: end, insertedText: "" }); + } + } else if (specifierCount === 1 && hasDefault && namedImports) { + edits.push(removeNamedImportsAfterDefault(namedImports, source)); + } else { + edits.push(removeNodeWithComma(spec, source)); + } + changed = true; + } + + for (const pattern of rootNode.findAll({ rule: { kind: "object_pattern" } })) { + const decl = pattern.ancestors().find((a) => a.kind() === "variable_declarator"); + const statement = decl ? nearestStatement(decl) : null; + const value = decl?.field("value"); + if (!decl || !statement) continue; + const destructuresReact = + (value?.kind() === "identifier" && reactNames.has(value.text())) || + isRequireCall(value ?? null, reactModules); + if (!destructuresReact) continue; + + const props = pattern.children().filter((child) => + child.kind() === "shorthand_property_identifier_pattern" || + child.kind() === "pair_pattern" + ); + const propTypesProp = props.find((prop) => + prop.text() === "PropTypes" || prop.field("key")?.text() === "PropTypes" + ); + if (!propTypesProp) continue; + + const nestedPattern = propTypesProp.field("value"); + if (nestedPattern?.kind() === "object_pattern") { + const childNames = identifierNamesInPattern(nestedPattern); + if (childNames.length > 0) { + edits.push({ + startPos: statement.range().start.index, + endPos: statement.range().start.index, + insertedText: `const { ${childNames.join(", ")} } = ${localPropTypesName};\n`, + }); + } + } else { + const alias = propTypesProp.field("value"); + if (alias?.kind() === "identifier") localPropTypesName = alias.text(); + } + + if (props.length === 1) { + const { start, end } = statementRange(statement, source); + edits.push({ startPos: start, endPos: end, insertedText: "" }); + } else { + edits.push(removeNodeWithComma(propTypesProp, source)); + } + changed = true; + } + + for (const member of rootNode.findAll({ rule: { kind: "member_expression" } })) { + const object = member.field("object"); + const property = member.field("property"); + if (!object || !property || property.text() !== "PropTypes") continue; + if (object.kind() !== "identifier" || !reactNames.has(object.text())) continue; + + const declarator = member.ancestors().find((a) => a.kind() === "variable_declarator"); + const name = declarator?.field("name"); + if (declarator && name?.kind() === "identifier" && name.text() === localPropTypesName) { + const statement = nearestStatement(declarator); + if (statement) { + const { start, end } = statementRangeWithLeadingLineComments(statement, source); + edits.push({ startPos: start, endPos: end, insertedText: "" }); + } + } else { + edits.push(member.replace(localPropTypesName)); + } + changed = true; + metric.increment({ file: metricFile(root.filename()) }); + } + + if (!changed) return null; + + if (!addedPropTypesImportByReplacement && !hasImportOrRequire(rootNode, moduleName)) { + if (usesImportSyntax(rootNode)) { + const pos = firstSortedImportPosition(rootNode, moduleName); + edits.push({ + startPos: pos, + endPos: pos, + insertedText: `import ${localPropTypesName} from '${moduleName}';\n`, + + }); + } else { + const pos = firstSortedRequirePosition(rootNode, moduleName); + const keyword = usesVarForRequires(rootNode) ? "var" : "const"; + edits.push({ + startPos: pos, + endPos: pos, + insertedText: `${keyword} ${localPropTypesName} = require('${moduleName}');\n`, + }); + } + } + + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/expected.tsx new file mode 100644 index 0000000..1ab2a24 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/expected.tsx @@ -0,0 +1,5 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const object = PropTypes.object; +const string = PropTypes.string; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/input.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/input.tsx new file mode 100644 index 0000000..36366cc --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/input.tsx @@ -0,0 +1,5 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const object = PropTypes.object; +const string = React.PropTypes.string; \ No newline at end of file diff --git a/react-proptypes-to-prop-types/tests/basic-prop-types/metrics.json b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/metrics.json similarity index 63% rename from react-proptypes-to-prop-types/tests/basic-prop-types/metrics.json rename to codemods/react-proptypes-to-prop-types/tests/already-migrated-import/metrics.json index 59c45c1..f6d3564 100644 --- a/react-proptypes-to-prop-types/tests/basic-prop-types/metrics.json +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/metrics.json @@ -2,7 +2,7 @@ "react-proptypes-migrations": [ { "cardinality": { - "file": "tests/basic-prop-types/input.tsx" + "file": "tests/already-migrated-import/input.tsx" }, "count": 1 } diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/expected.tsx new file mode 100644 index 0000000..61771ed --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/expected.tsx @@ -0,0 +1,5 @@ +import { PropTypes as PT } from 'prop-types'; +import React from 'react'; + +const object = PropTypes.object; +const string = PropTypes.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/input.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/input.tsx new file mode 100644 index 0000000..53b00e1 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/input.tsx @@ -0,0 +1,5 @@ +import { PropTypes as PT } from 'prop-types'; +import React from 'react'; + +const object = PropTypes.object; +const string = React.PropTypes.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/metrics.json b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/metrics.json new file mode 100644 index 0000000..ff7379a --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/already-migrated-named-as-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/expected.tsx new file mode 100644 index 0000000..d6a9a96 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/expected.tsx @@ -0,0 +1,5 @@ +import { PropTypes } from 'prop-types'; +import React from 'react'; + +const object = PropTypes.object; +const string = PropTypes.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/input.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/input.tsx new file mode 100644 index 0000000..2cd97a1 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/input.tsx @@ -0,0 +1,5 @@ +import { PropTypes } from 'prop-types'; +import React from 'react'; + +const object = PropTypes.object; +const string = React.PropTypes.string; diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/metrics.json b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/metrics.json new file mode 100644 index 0000000..5cb2111 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/already-migrated-named-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/expected.tsx new file mode 100644 index 0000000..9726ce1 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/expected.tsx @@ -0,0 +1,5 @@ +const PropTypes = require('prop-types'); +const React = require('React'); + +const object = PropTypes.object; +const string = PropTypes.string; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/input.tsx b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/input.tsx new file mode 100644 index 0000000..f4eb188 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/input.tsx @@ -0,0 +1,5 @@ +const PropTypes = require('prop-types'); +const React = require('React'); + +const object = PropTypes.object; +const string = React.PropTypes.string; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/metrics.json b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/metrics.json new file mode 100644 index 0000000..3eda7ec --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/already-migrated-require/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/expected.tsx new file mode 100644 index 0000000..7071006 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/expected.tsx @@ -0,0 +1,5 @@ +const PropTypes = require('prop-types'); +const React = require('react'); +// This should remain, modified +const string = PropTypes.string; +const bool = PropTypes.bool; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/input.tsx b/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/input.tsx new file mode 100644 index 0000000..03ff11b --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/input.tsx @@ -0,0 +1,9 @@ +const React = require('react'); + +// These are no longer needed after the transform +const PropTypes = React.PropTypes; +const {PropTypes} = React; + +// This should remain, modified +const string = React.PropTypes.string; +const bool = PropTypes.bool; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/metrics.json b/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/metrics.json new file mode 100644 index 0000000..fe7592c --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/assigned-from-react-var/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx new file mode 100644 index 0000000..24e0ce0 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx @@ -0,0 +1,6 @@ +const PropTypes = require('prop-types'); +const React = require('react'); + +const ReactPropTypes = PropTypes; +const foo = ReactPropTypes.string; +const bar = PropTypes.string; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/input.tsx b/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/input.tsx new file mode 100644 index 0000000..55bdf39 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/input.tsx @@ -0,0 +1,7 @@ +const React = require('react'); + +const ReactPropTypes = React.PropTypes; +const PropTypes = React.PropTypes; + +const foo = ReactPropTypes.string; +const bar = PropTypes.string; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/metrics.json b/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/metrics.json new file mode 100644 index 0000000..45447b2 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/assigned-to-var-with-different-name/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx new file mode 100644 index 0000000..ba61a76 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +class ClassComponent extends Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/input.tsx b/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/input.tsx new file mode 100644 index 0000000..466c1e2 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/input.tsx @@ -0,0 +1,17 @@ +import React, { Component, PropTypes } from 'react'; + +class ClassComponent extends Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx new file mode 100644 index 0000000..15e1bf3 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx @@ -0,0 +1,22 @@ +import Abc from 'abc'; +import PropTypes from 'prop-types'; +import React from 'React'; +import Xzy from 'xyz'; + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + const { text, ...rest } = this.props; + return {text}; + } +} + +function FunctionalComponent (props) { + const { text, ...rest } = props; + return {text}; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/default-import/input.tsx b/codemods/react-proptypes-to-prop-types/tests/default-import/input.tsx new file mode 100644 index 0000000..0ffc10a --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/default-import/input.tsx @@ -0,0 +1,21 @@ +import Abc from 'abc'; +import React from 'React'; +import Xzy from 'xyz'; + +class ClassComponent extends React.Component { + static propTypes = { + text: React.PropTypes.string.isRequired, + }; + render() { + const { text, ...rest } = this.props; + return {text}; + } +} + +function FunctionalComponent (props) { + const { text, ...rest } = props; + return {text}; +} +FunctionalComponent.propTypes = { + text: React.PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/react-proptypes-to-prop-types/tests/multiple-types/metrics.json b/codemods/react-proptypes-to-prop-types/tests/default-import/metrics.json similarity index 67% rename from react-proptypes-to-prop-types/tests/multiple-types/metrics.json rename to codemods/react-proptypes-to-prop-types/tests/default-import/metrics.json index 50d7232..8af3ca6 100644 --- a/react-proptypes-to-prop-types/tests/multiple-types/metrics.json +++ b/codemods/react-proptypes-to-prop-types/tests/default-import/metrics.json @@ -2,7 +2,7 @@ "react-proptypes-migrations": [ { "cardinality": { - "file": "tests/multiple-types/input.tsx" + "file": "tests/default-import/input.tsx" }, "count": 2 } diff --git a/codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/expected.tsx new file mode 100644 index 0000000..75133a7 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/expected.tsx @@ -0,0 +1,5 @@ +import PropTypes from 'prop-types'; + +export default PropTypes.shape({ + id: PropTypes.string, +}) diff --git a/codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/input.tsx b/codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/input.tsx new file mode 100644 index 0000000..48b1fa7 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/input.tsx @@ -0,0 +1,5 @@ +import { PropTypes } from 'react'; + +export default PropTypes.shape({ + id: PropTypes.string, +}) diff --git a/codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx new file mode 100644 index 0000000..317f7e5 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx @@ -0,0 +1,8 @@ +import PT from 'prop-types'; +import React, { Component } from 'react'; + +class ClassComponent extends Component { + static propTypes = { + foo: PT.string.isRequired, + } +} diff --git a/codemods/react-proptypes-to-prop-types/tests/import-alias/input.tsx b/codemods/react-proptypes-to-prop-types/tests/import-alias/input.tsx new file mode 100644 index 0000000..61ec4d1 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/import-alias/input.tsx @@ -0,0 +1,7 @@ +import React, { Component, PropTypes as PT } from 'react'; + +class ClassComponent extends Component { + static propTypes = { + foo: PT.string.isRequired, + } +} diff --git a/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx new file mode 100644 index 0000000..9c5701c --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx @@ -0,0 +1,4 @@ +const PropTypes = require('prop-types'); +const React = require('React'); + +import type {SomeType} from 'SomeModule'; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/input.tsx b/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/input.tsx new file mode 100644 index 0000000..03286f9 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/input.tsx @@ -0,0 +1,5 @@ +const React = require('React'); + +const {PropTypes} = React; + +import type {SomeType} from 'SomeModule'; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx new file mode 100644 index 0000000..3845188 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx @@ -0,0 +1,14 @@ +'use strict'; + +import type { Foo } from 'Foo'; + +const PropTypes = require('prop-types'); +const React = require('React'); + +class Component extends React.PureComponent { + render() { + return
; + } +} + +module.exports = Component; diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/input.tsx b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/input.tsx new file mode 100644 index 0000000..053b76b --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/input.tsx @@ -0,0 +1,15 @@ +'use strict'; + +import type { Foo } from 'Foo'; + +const React = require('React'); + +const {PropTypes} = React; + +class Component extends React.PureComponent { + render() { + return
; + } +} + +module.exports = Component; diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx new file mode 100644 index 0000000..8787e82 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx @@ -0,0 +1,10 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +require('styles.css'); + +class MyClass extends React.Component {} + +MyClass.propTypes = { + foo: PropTypes.string +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/input.tsx b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/input.tsx new file mode 100644 index 0000000..fba7ee3 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/input.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +require('styles.css'); + +class MyClass extends React.Component {} + +MyClass.propTypes = { + foo: React.PropTypes.string +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/metrics.json b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/metrics.json new file mode 100644 index 0000000..7402b04 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/mixed-import-and-require/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx new file mode 100644 index 0000000..e78e65e --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx @@ -0,0 +1,18 @@ +const PropTypes = require('PropTypes'); +const React = require('React'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/module-name/input.tsx b/codemods/react-proptypes-to-prop-types/tests/module-name/input.tsx new file mode 100644 index 0000000..b0bd1ef --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/module-name/input.tsx @@ -0,0 +1,17 @@ +const React = require('React'); + +class ClassComponent extends React.Component { + static propTypes = { + text: React.PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: React.PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/react-proptypes-to-prop-types/tests/array-and-object/metrics.json b/codemods/react-proptypes-to-prop-types/tests/module-name/metrics.json similarity index 55% rename from react-proptypes-to-prop-types/tests/array-and-object/metrics.json rename to codemods/react-proptypes-to-prop-types/tests/module-name/metrics.json index 409488c..1d5d4ab 100644 --- a/react-proptypes-to-prop-types/tests/array-and-object/metrics.json +++ b/codemods/react-proptypes-to-prop-types/tests/module-name/metrics.json @@ -2,9 +2,9 @@ "react-proptypes-migrations": [ { "cardinality": { - "file": "tests/array-and-object/input.tsx" + "file": "tests/module-name/input.tsx" }, - "count": 3 + "count": 2 } ] } \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/module-name/test.config.json b/codemods/react-proptypes-to-prop-types/tests/module-name/test.config.json new file mode 100644 index 0000000..79a5de0 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/module-name/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "module-name": "PropTypes" + } +} diff --git a/codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx new file mode 100644 index 0000000..7e1fd2f --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import classnames from 'classnames'; + +const propTypes = { + children: PropTypes.node, + className: PropTypes.string, +}; + +const Accordion = ({ children, className }) => { + const classNames = classnames('bx--accordion', className); + return ( +
    + {children} +
+ ); +}; + +Accordion.propTypes = propTypes; + +export default Accordion; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/named-parameters/input.tsx b/codemods/react-proptypes-to-prop-types/tests/named-parameters/input.tsx new file mode 100644 index 0000000..d316f50 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/named-parameters/input.tsx @@ -0,0 +1,20 @@ +import React, { PropTypes } from 'react'; +import classnames from 'classnames'; + +const propTypes = { + children: PropTypes.node, + className: PropTypes.string, +}; + +const Accordion = ({ children, className }) => { + const classNames = classnames('bx--accordion', className); + return ( +
    + {children} +
+ ); +}; + +Accordion.propTypes = propTypes; + +export default Accordion; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx new file mode 100644 index 0000000..bc39982 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx @@ -0,0 +1,7 @@ +import PropTypes from 'prop-types'; +import React from 'react'; + +const { shape, bool, number, string } = PropTypes; +const { + Component, + } = React; diff --git a/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/input.tsx b/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/input.tsx new file mode 100644 index 0000000..27d7543 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/input.tsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const { + Component, + PropTypes: { + shape, + bool, + number, + string, + }, +} = React; diff --git a/codemods/react-proptypes-to-prop-types/tests/no-change-import/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/no-change-import/expected.tsx new file mode 100644 index 0000000..0ed892a --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/no-change-import/expected.tsx @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types'; +import React from 'React'; + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/no-change-import/input.tsx b/codemods/react-proptypes-to-prop-types/tests/no-change-import/input.tsx new file mode 100644 index 0000000..0ed892a --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/no-change-import/input.tsx @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types'; +import React from 'React'; + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/no-change-require/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/no-change-require/expected.tsx new file mode 100644 index 0000000..463e982 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/no-change-require/expected.tsx @@ -0,0 +1,18 @@ +const PropTypes = require('prop-types'); +const React = require('React'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/no-change-require/input.tsx b/codemods/react-proptypes-to-prop-types/tests/no-change-require/input.tsx new file mode 100644 index 0000000..463e982 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/no-change-require/input.tsx @@ -0,0 +1,18 @@ +const PropTypes = require('prop-types'); +const React = require('React'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx new file mode 100644 index 0000000..0a7e190 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx @@ -0,0 +1,9 @@ +const PT = require('prop-types'); +const React = require('react'); +const { Component } = React; + +class ClassComponent extends Component { + static propTypes = { + foo: PT.string.isRequired, + } +} diff --git a/codemods/react-proptypes-to-prop-types/tests/require-alias/input.tsx b/codemods/react-proptypes-to-prop-types/tests/require-alias/input.tsx new file mode 100644 index 0000000..3d4f7b3 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require-alias/input.tsx @@ -0,0 +1,8 @@ +const React = require('react'); +const { Component, PropTypes: PT } = React; + +class ClassComponent extends Component { + static propTypes = { + foo: PT.string.isRequired, + } +} diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx new file mode 100644 index 0000000..b8672d9 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx @@ -0,0 +1,8 @@ +const PropTypes = require('prop-types'); +const React = require('react'); +function Foo(props) { + return
{props.text}
; +} +Foo.propTypes = { + text: PropTypes.string.isRequired, +}; diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/input.tsx b/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/input.tsx new file mode 100644 index 0000000..4fde2f4 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/input.tsx @@ -0,0 +1,9 @@ +const React = require('react'); +const { PropTypes } = require('react'); + +function Foo(props) { + return
{props.text}
; +} +Foo.propTypes = { + text: PropTypes.string.isRequired, +}; diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx new file mode 100644 index 0000000..5491a87 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx @@ -0,0 +1,19 @@ +const PropTypes = require('prop-types'); +const React = require('react'); +const { Component } = React; + +class ClassComponent extends Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/input.tsx b/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/input.tsx new file mode 100644 index 0000000..2db70ad --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/input.tsx @@ -0,0 +1,18 @@ +const React = require('react'); +const { Component, PropTypes } = React; + +class ClassComponent extends Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx new file mode 100644 index 0000000..3d1d184 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx @@ -0,0 +1,20 @@ +const Abc = require('abc'); +const PropTypes = require('prop-types'); +const React = require('react'); +const Xyz = require('xyz'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return {this.props.text}; + } +} + +function FunctionalComponent (props) { + return {props.text}; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/input.tsx b/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/input.tsx new file mode 100644 index 0000000..3bf0664 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/input.tsx @@ -0,0 +1,20 @@ +const Abc = require('abc'); +const React = require('react'); +const { PropTypes } = React; +const Xyz = require('xyz'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return {this.props.text}; + } +} + +function FunctionalComponent (props) { + return {props.text}; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/require/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/require/expected.tsx new file mode 100644 index 0000000..463e982 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require/expected.tsx @@ -0,0 +1,18 @@ +const PropTypes = require('prop-types'); +const React = require('React'); + +class ClassComponent extends React.Component { + static propTypes = { + text: PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/require/input.tsx b/codemods/react-proptypes-to-prop-types/tests/require/input.tsx new file mode 100644 index 0000000..b0bd1ef --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require/input.tsx @@ -0,0 +1,17 @@ +const React = require('React'); + +class ClassComponent extends React.Component { + static propTypes = { + text: React.PropTypes.string.isRequired, + }; + render() { + return
{this.props.text}
; + } +} + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: React.PropTypes.string.isRequired, +}; \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/require/metrics.json b/codemods/react-proptypes-to-prop-types/tests/require/metrics.json new file mode 100644 index 0000000..e73a0dd --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/require/metrics.json @@ -0,0 +1,10 @@ +{ + "react-proptypes-migrations": [ + { + "cardinality": { + "file": "tests/require/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx b/codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx new file mode 100644 index 0000000..8ab3354 --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx @@ -0,0 +1,12 @@ +/** + * This is some multiline comment + */ +import PropTypes from 'prop-types'; +import React from 'react'; + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; diff --git a/codemods/react-proptypes-to-prop-types/tests/with-top-comment/input.tsx b/codemods/react-proptypes-to-prop-types/tests/with-top-comment/input.tsx new file mode 100644 index 0000000..a1adccf --- /dev/null +++ b/codemods/react-proptypes-to-prop-types/tests/with-top-comment/input.tsx @@ -0,0 +1,11 @@ +/** + * This is some multiline comment + */ +import React, { PropTypes } from 'react'; + +function FunctionalComponent (props) { + return
{props.text}
; +} +FunctionalComponent.propTypes = { + text: PropTypes.string.isRequired, +}; diff --git a/find-dom-node/tsconfig.json b/codemods/react-proptypes-to-prop-types/tsconfig.json similarity index 61% rename from find-dom-node/tsconfig.json rename to codemods/react-proptypes-to-prop-types/tsconfig.json index 469fc5a..decec57 100644 --- a/find-dom-node/tsconfig.json +++ b/codemods/react-proptypes-to-prop-types/tsconfig.json @@ -1,11 +1,19 @@ { "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], "module": "NodeNext", "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], + "types": [ + "@codemod.com/jssg-types", + "node" + ], "allowImportingTsExtensions": true, "noEmit": true, - "verbatimModuleSyntax": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, "erasableSyntaxOnly": true, "strict": true, "strictNullChecks": true, @@ -13,5 +21,7 @@ "noFallthroughCasesInSwitch": true, "noUncheckedIndexedAccess": true }, - "exclude": ["tests"] + "exclude": [ + "tests" + ] } diff --git a/sort-comp/workflow.yaml b/codemods/react-proptypes-to-prop-types/workflow.yaml similarity index 65% rename from sort-comp/workflow.yaml rename to codemods/react-proptypes-to-prop-types/workflow.yaml index 8773841..c39db7c 100644 --- a/sort-comp/workflow.yaml +++ b/codemods/react-proptypes-to-prop-types/workflow.yaml @@ -2,6 +2,14 @@ version: "1" +params: + schema: + module-name: + name: "Module Name" + description: "Module specifier to use for PropTypes imports and requires." + type: string + default: "prop-types" + nodes: - id: apply-transforms name: Apply AST Transformations diff --git a/rename-unsafe-lifecycles/.gitignore b/codemods/rename-unsafe-lifecycles/.gitignore similarity index 100% rename from rename-unsafe-lifecycles/.gitignore rename to codemods/rename-unsafe-lifecycles/.gitignore diff --git a/codemods/rename-unsafe-lifecycles/README.md b/codemods/rename-unsafe-lifecycles/README.md new file mode 100644 index 0000000..4ccaec6 --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/README.md @@ -0,0 +1,16 @@ +# @react/rename-unsafe-lifecycles + +Rename deprecated React lifecycle names to their `UNSAFE_` equivalents in class components, object literals, and member references. + +## Usage + +```bash +codemod run @react/rename-unsafe-lifecycles +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/rename-unsafe-lifecycles/codemod.yaml b/codemods/rename-unsafe-lifecycles/codemod.yaml similarity index 69% rename from rename-unsafe-lifecycles/codemod.yaml rename to codemods/rename-unsafe-lifecycles/codemod.yaml index 1893f91..07f1e6f 100644 --- a/rename-unsafe-lifecycles/codemod.yaml +++ b/codemods/rename-unsafe-lifecycles/codemod.yaml @@ -1,9 +1,9 @@ schema_version: "1.0" -name: "@react-codemods/rename-unsafe-lifecycles" +name: "@react/rename-unsafe-lifecycles" version: "0.1.0" description: "Rename deprecated lifecycle methods to UNSAFE_ prefixed versions" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" @@ -11,7 +11,7 @@ workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration"] +keywords: ["React"] registry: access: "public" diff --git a/rename-unsafe-lifecycles/package.json b/codemods/rename-unsafe-lifecycles/package.json similarity index 76% rename from rename-unsafe-lifecycles/package.json rename to codemods/rename-unsafe-lifecycles/package.json index c41c9a3..a8fed9d 100644 --- a/rename-unsafe-lifecycles/package.json +++ b/codemods/rename-unsafe-lifecycles/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/rename-unsafe-lifecycles", + "name": "@react/rename-unsafe-lifecycles", "version": "0.1.0", "description": "Rename deprecated lifecycle methods to UNSAFE_ prefixed versions", "type": "module", @@ -9,6 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" } } diff --git a/rename-unsafe-lifecycles/pnpm-lock.yaml b/codemods/rename-unsafe-lifecycles/pnpm-lock.yaml similarity index 100% rename from rename-unsafe-lifecycles/pnpm-lock.yaml rename to codemods/rename-unsafe-lifecycles/pnpm-lock.yaml diff --git a/rename-unsafe-lifecycles/scripts/codemod.ts b/codemods/rename-unsafe-lifecycles/scripts/codemod.ts similarity index 93% rename from rename-unsafe-lifecycles/scripts/codemod.ts rename to codemods/rename-unsafe-lifecycles/scripts/codemod.ts index 98b727f..d65d934 100644 --- a/rename-unsafe-lifecycles/scripts/codemod.ts +++ b/codemods/rename-unsafe-lifecycles/scripts/codemod.ts @@ -2,6 +2,12 @@ import type { Transform, Edit } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + + const DEPRECATED_LIFECYCLES = { componentWillMount: "UNSAFE_componentWillMount", componentWillReceiveProps: "UNSAFE_componentWillReceiveProps", @@ -110,7 +116,7 @@ const transform: Transform = async (root) => { const total = Object.values(renameCount).reduce((a, b) => a + b, 0); transformMetric.increment({ lifecycles, - file: root.filename(), + file: metricFile(root.filename()), }, total); } diff --git a/rename-unsafe-lifecycles/tests/original-arrow-functions/expected.tsx b/codemods/rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx similarity index 99% rename from rename-unsafe-lifecycles/tests/original-arrow-functions/expected.tsx rename to codemods/rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx index d87c71a..8087f10 100644 --- a/rename-unsafe-lifecycles/tests/original-arrow-functions/expected.tsx +++ b/codemods/rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx @@ -11,4 +11,4 @@ class Component extends React.Component { render() { return null; } -} +} \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/original-arrow-functions/input.tsx b/codemods/rename-unsafe-lifecycles/tests/arrow-functions/input.tsx similarity index 99% rename from rename-unsafe-lifecycles/tests/original-arrow-functions/input.tsx rename to codemods/rename-unsafe-lifecycles/tests/arrow-functions/input.tsx index 7bf3094..501cd6c 100644 --- a/rename-unsafe-lifecycles/tests/original-arrow-functions/input.tsx +++ b/codemods/rename-unsafe-lifecycles/tests/arrow-functions/input.tsx @@ -11,4 +11,4 @@ class Component extends React.Component { render() { return null; } -} +} \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/class-methods/metrics.json b/codemods/rename-unsafe-lifecycles/tests/arrow-functions/metrics.json similarity index 78% rename from rename-unsafe-lifecycles/tests/class-methods/metrics.json rename to codemods/rename-unsafe-lifecycles/tests/arrow-functions/metrics.json index 3c9ca7a..56a8030 100644 --- a/rename-unsafe-lifecycles/tests/class-methods/metrics.json +++ b/codemods/rename-unsafe-lifecycles/tests/arrow-functions/metrics.json @@ -2,7 +2,7 @@ "lifecycle-renames": [ { "cardinality": { - "file": "tests/class-methods/input.tsx", + "file": "tests/arrow-functions/input.tsx", "lifecycles": "componentWillMount,componentWillReceiveProps,componentWillUpdate" }, "count": 3 diff --git a/rename-unsafe-lifecycles/tests/original-create-react-class/expected.tsx b/codemods/rename-unsafe-lifecycles/tests/create-react-class/expected.tsx similarity index 99% rename from rename-unsafe-lifecycles/tests/original-create-react-class/expected.tsx rename to codemods/rename-unsafe-lifecycles/tests/create-react-class/expected.tsx index 8949876..f9eab80 100644 --- a/rename-unsafe-lifecycles/tests/original-create-react-class/expected.tsx +++ b/codemods/rename-unsafe-lifecycles/tests/create-react-class/expected.tsx @@ -45,4 +45,4 @@ const MyComponent = createReactClass({ render() { // render }, -}); +}); \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/original-create-react-class/input.tsx b/codemods/rename-unsafe-lifecycles/tests/create-react-class/input.tsx similarity index 99% rename from rename-unsafe-lifecycles/tests/original-create-react-class/input.tsx rename to codemods/rename-unsafe-lifecycles/tests/create-react-class/input.tsx index e1a661f..5708b2d 100644 --- a/rename-unsafe-lifecycles/tests/original-create-react-class/input.tsx +++ b/codemods/rename-unsafe-lifecycles/tests/create-react-class/input.tsx @@ -45,4 +45,4 @@ const MyComponent = createReactClass({ render() { // render }, -}); +}); \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/original-create-react-class/metrics.json b/codemods/rename-unsafe-lifecycles/tests/create-react-class/metrics.json similarity index 74% rename from rename-unsafe-lifecycles/tests/original-create-react-class/metrics.json rename to codemods/rename-unsafe-lifecycles/tests/create-react-class/metrics.json index c31d44c..68c0180 100644 --- a/rename-unsafe-lifecycles/tests/original-create-react-class/metrics.json +++ b/codemods/rename-unsafe-lifecycles/tests/create-react-class/metrics.json @@ -2,7 +2,7 @@ "lifecycle-renames": [ { "cardinality": { - "file": "tests/original-create-react-class/input.tsx", + "file": "tests/create-react-class/input.tsx", "lifecycles": "componentWillMount,componentWillReceiveProps,componentWillUpdate" }, "count": 6 diff --git a/rename-unsafe-lifecycles/tests/original-instance-methods/expected.tsx b/codemods/rename-unsafe-lifecycles/tests/instance-methods/expected.tsx similarity index 99% rename from rename-unsafe-lifecycles/tests/original-instance-methods/expected.tsx rename to codemods/rename-unsafe-lifecycles/tests/instance-methods/expected.tsx index 78915c9..610364c 100644 --- a/rename-unsafe-lifecycles/tests/original-instance-methods/expected.tsx +++ b/codemods/rename-unsafe-lifecycles/tests/instance-methods/expected.tsx @@ -22,4 +22,4 @@ class ClassComponent extends React.Component { render() { // render } -} +} \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/original-instance-methods/input.tsx b/codemods/rename-unsafe-lifecycles/tests/instance-methods/input.tsx similarity index 99% rename from rename-unsafe-lifecycles/tests/original-instance-methods/input.tsx rename to codemods/rename-unsafe-lifecycles/tests/instance-methods/input.tsx index 28ae3de..619bf50 100644 --- a/rename-unsafe-lifecycles/tests/original-instance-methods/input.tsx +++ b/codemods/rename-unsafe-lifecycles/tests/instance-methods/input.tsx @@ -22,4 +22,4 @@ class ClassComponent extends React.Component { render() { // render } -} +} \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/original-arrow-functions/metrics.json b/codemods/rename-unsafe-lifecycles/tests/instance-methods/metrics.json similarity index 75% rename from rename-unsafe-lifecycles/tests/original-arrow-functions/metrics.json rename to codemods/rename-unsafe-lifecycles/tests/instance-methods/metrics.json index 7cf8ed9..d59cca0 100644 --- a/rename-unsafe-lifecycles/tests/original-arrow-functions/metrics.json +++ b/codemods/rename-unsafe-lifecycles/tests/instance-methods/metrics.json @@ -2,7 +2,7 @@ "lifecycle-renames": [ { "cardinality": { - "file": "tests/original-arrow-functions/input.tsx", + "file": "tests/instance-methods/input.tsx", "lifecycles": "componentWillMount,componentWillReceiveProps,componentWillUpdate" }, "count": 3 diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/expected.tsx b/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/expected.tsx new file mode 100644 index 0000000..f17b3e7 --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/expected.tsx @@ -0,0 +1,3 @@ +const instance = new Component(); +instance.UNSAFE_componentWillMount(); +instance.componentDidMount(); \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/input.tsx b/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/input.tsx new file mode 100644 index 0000000..d4d7835 --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/input.tsx @@ -0,0 +1,3 @@ +const instance = new Component(); +instance.componentWillMount(); +instance.componentDidMount(); \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/arrow-functions/metrics.json b/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/metrics.json similarity index 68% rename from rename-unsafe-lifecycles/tests/arrow-functions/metrics.json rename to codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/metrics.json index acc1a00..a91256f 100644 --- a/rename-unsafe-lifecycles/tests/arrow-functions/metrics.json +++ b/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/metrics.json @@ -2,7 +2,7 @@ "lifecycle-renames": [ { "cardinality": { - "file": "tests/arrow-functions/input.tsx", + "file": "tests/manually-calling-lifecycles/input.tsx", "lifecycles": "componentWillMount" }, "count": 1 diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/expected.tsx b/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/expected.tsx new file mode 100644 index 0000000..4af350b --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/expected.tsx @@ -0,0 +1,20 @@ +const React = require('react'); +const SomeOtherComponent = require('SomeOtherComponent'); + +class PMTAuthenticationFailureDialog extends React.Component { + componentDidMount(): void { + SomeOtherComponent.mixin.componentDidMount.apply(this); + } + + UNSAFE_componentWillMount(): void { + SomeOtherComponent.mixin.UNSAFE_componentWillMount.apply(this); + } + + componentWillUnmount(): void { + SomeOtherComponent.mixin.componentWillUnmount.apply(this); + } + + render() { + return null; + } +} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/input.tsx b/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/input.tsx new file mode 100644 index 0000000..a746d81 --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/input.tsx @@ -0,0 +1,20 @@ +const React = require('react'); +const SomeOtherComponent = require('SomeOtherComponent'); + +class PMTAuthenticationFailureDialog extends React.Component { + componentDidMount(): void { + SomeOtherComponent.mixin.componentDidMount.apply(this); + } + + componentWillMount(): void { + SomeOtherComponent.mixin.componentWillMount.apply(this); + } + + componentWillUnmount(): void { + SomeOtherComponent.mixin.componentWillUnmount.apply(this); + } + + render() { + return null; + } +} \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/member-expression-call/metrics.json b/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/metrics.json similarity index 67% rename from rename-unsafe-lifecycles/tests/member-expression-call/metrics.json rename to codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/metrics.json index 12f8955..d83605b 100644 --- a/rename-unsafe-lifecycles/tests/member-expression-call/metrics.json +++ b/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/metrics.json @@ -2,7 +2,7 @@ "lifecycle-renames": [ { "cardinality": { - "file": "tests/member-expression-call/input.tsx", + "file": "tests/manually-invoked-mixin-methods/input.tsx", "lifecycles": "componentWillMount" }, "count": 2 diff --git a/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/expected.tsx b/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/expected.tsx new file mode 100644 index 0000000..29c5734 --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/expected.tsx @@ -0,0 +1,11 @@ +const React = require('React'); + +class Component extends React.Component { + UNSAFE_componentWillMount() { + this.UNSAFE_componentWillReceiveProps(this.props); + } + UNSAFE_componentWillReceiveProps() {} + render() { + return null; + } +} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/input.tsx b/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/input.tsx new file mode 100644 index 0000000..b4b12dd --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/input.tsx @@ -0,0 +1,11 @@ +const React = require('React'); + +class Component extends React.Component { + componentWillMount() { + this.componentWillReceiveProps(this.props); + } + componentWillReceiveProps() {} + render() { + return null; + } +} \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/object-properties/metrics.json b/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/metrics.json similarity index 64% rename from rename-unsafe-lifecycles/tests/object-properties/metrics.json rename to codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/metrics.json index 74deee0..7997937 100644 --- a/rename-unsafe-lifecycles/tests/object-properties/metrics.json +++ b/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/metrics.json @@ -2,10 +2,10 @@ "lifecycle-renames": [ { "cardinality": { - "file": "tests/object-properties/input.tsx", + "file": "tests/one-lifecycle-calls-another/input.tsx", "lifecycles": "componentWillMount,componentWillReceiveProps" }, - "count": 2 + "count": 3 } ] } \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/standalone-function/expected.tsx b/codemods/rename-unsafe-lifecycles/tests/standalone-function/expected.tsx new file mode 100644 index 0000000..c515ccc --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/standalone-function/expected.tsx @@ -0,0 +1,9 @@ +function componentWillMount() { + // componentWillMount +} +function componentWillUpdate(nextProps, nextState) { + // componentWillUpdate +} +function componentWillReceiveProps(nextProps) { + // componentWillReceiveProps +} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/standalone-function/input.tsx b/codemods/rename-unsafe-lifecycles/tests/standalone-function/input.tsx new file mode 100644 index 0000000..c515ccc --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/standalone-function/input.tsx @@ -0,0 +1,9 @@ +function componentWillMount() { + // componentWillMount +} +function componentWillUpdate(nextProps, nextState) { + // componentWillUpdate +} +function componentWillReceiveProps(nextProps) { + // componentWillReceiveProps +} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/expected.tsx b/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/expected.tsx new file mode 100644 index 0000000..13f1501 --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/expected.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +class Component extends React.Component { + public UNSAFE_componentWillMount() {} + public componentDidMount() {} + public UNSAFE_componentWillReceiveProps() {} + public shouldComponentUpdate() {} + public UNSAFE_componentWillUpdate() {} + public componentDidUpdate() {} + public componentWillUnmount() {} + public render() { + return null; + } +} diff --git a/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/input.tsx b/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/input.tsx new file mode 100644 index 0000000..5afdeed --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/input.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +class Component extends React.Component { + public componentWillMount() {} + public componentDidMount() {} + public componentWillReceiveProps() {} + public shouldComponentUpdate() {} + public componentWillUpdate() {} + public componentDidUpdate() {} + public componentWillUnmount() {} + public render() { + return null; + } +} diff --git a/rename-unsafe-lifecycles/tests/original-instance-methods/metrics.json b/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/metrics.json similarity index 74% rename from rename-unsafe-lifecycles/tests/original-instance-methods/metrics.json rename to codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/metrics.json index 10cc361..4119909 100644 --- a/rename-unsafe-lifecycles/tests/original-instance-methods/metrics.json +++ b/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/metrics.json @@ -2,7 +2,7 @@ "lifecycle-renames": [ { "cardinality": { - "file": "tests/original-instance-methods/input.tsx", + "file": "tests/typescript-class-tsx/input.tsx", "lifecycles": "componentWillMount,componentWillReceiveProps,componentWillUpdate" }, "count": 3 diff --git a/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/expected.tsx b/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/expected.tsx new file mode 100644 index 0000000..eb8979a --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/expected.tsx @@ -0,0 +1,7 @@ +class SomeClass { + someMethod() { + const componentWillMount = true; + let componentWillUpdate = 123; + var componentWillReceiveProps = 'abc'; + } +} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/input.tsx b/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/input.tsx new file mode 100644 index 0000000..eb8979a --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/input.tsx @@ -0,0 +1,7 @@ +class SomeClass { + someMethod() { + const componentWillMount = true; + let componentWillUpdate = 123; + var componentWillReceiveProps = 'abc'; + } +} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tsconfig.json b/codemods/rename-unsafe-lifecycles/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/rename-unsafe-lifecycles/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/pure-render-mixin/workflow.yaml b/codemods/rename-unsafe-lifecycles/workflow.yaml similarity index 100% rename from pure-render-mixin/workflow.yaml rename to codemods/rename-unsafe-lifecycles/workflow.yaml diff --git a/codemods/replace-act-import/README.md b/codemods/replace-act-import/README.md new file mode 100644 index 0000000..14085c2 --- /dev/null +++ b/codemods/replace-act-import/README.md @@ -0,0 +1,16 @@ +# @react/replace-act-import + +Move `act` usage from `react-dom/test-utils` to `react` across named imports, namespace imports, and re-exports. + +## Usage + +```bash +codemod run @react/replace-act-import +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/replace-act-import/codemod.yaml b/codemods/replace-act-import/codemod.yaml similarity index 66% rename from replace-act-import/codemod.yaml rename to codemods/replace-act-import/codemod.yaml index 77d19ec..1570066 100644 --- a/replace-act-import/codemod.yaml +++ b/codemods/replace-act-import/codemod.yaml @@ -1,16 +1,16 @@ schema_version: "1.0" -name: "@react-codemods/replace-act-import" +name: "@react/replace-act-import" version: "0.1.0" description: "Migrate act() from react-dom/test-utils to react" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration", "react", "act"] +keywords: ["React"] registry: access: "public" diff --git a/replace-act-import/package.json b/codemods/replace-act-import/package.json similarity index 79% rename from replace-act-import/package.json rename to codemods/replace-act-import/package.json index 5403fc2..bc9cda9 100644 --- a/replace-act-import/package.json +++ b/codemods/replace-act-import/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/replace-act-import", + "name": "@react/replace-act-import", "version": "0.1.0", "description": "Migrate act() from react-dom/test-utils to react", "type": "module", @@ -9,7 +9,8 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" }, "dependencies": { "@jssg/utils": "^0.0.2" diff --git a/replace-act-import/scripts/codemod.ts b/codemods/replace-act-import/scripts/codemod.ts similarity index 66% rename from replace-act-import/scripts/codemod.ts rename to codemods/replace-act-import/scripts/codemod.ts index 46022ec..b9003c2 100644 --- a/replace-act-import/scripts/codemod.ts +++ b/codemods/replace-act-import/scripts/codemod.ts @@ -3,6 +3,11 @@ import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; import { getImport, addImport, removeImport } from "@jssg/utils/javascript/imports"; +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + const TEST_UTILS_MODULE = "react-dom/test-utils"; const REACT_MODULE = "react"; @@ -12,7 +17,11 @@ function getTestUtilsImport(rootNode: SgNode): (TestUtilsImport const defaultImp = getImport(rootNode, { type: "default", from: TEST_UTILS_MODULE }); if (defaultImp) { const importNode = defaultImp.node.ancestors().find((a) => a.kind() === "import_statement"); - return { name: defaultImp.alias, type: "default", importNode: importNode ?? undefined }; + return { + name: defaultImp.alias, + type: defaultImp.isNamespace ? "namespace" : "default", + importNode: importNode ?? undefined, + }; } const importDecls = rootNode.findAll({ @@ -92,27 +101,51 @@ const transform: Transform = async (root) => { const objNode = member.field("object"); if (objNode && objNode.text() === testUtilsImport.name) { edits.push(objNode.replace(reactName)); - metric.increment({ file: root.filename(), pattern: "member-call" }); + metric.increment({ file: metricFile(root.filename()), pattern: "member-call" }); } } } } if (actNamedImport && !actNamedImport.isNamespace) { - const addEdit = addImport(rootNode, { - type: "named", - specifiers: [{ name: "act", alias: actNamedImport.alias }], - from: REACT_MODULE, - }); - if (addEdit) edits.push(addEdit); + const importNode = actNamedImport.node.ancestors().find((a) => a.kind() === "import_statement"); + const specifierCount = importNode?.findAll({ rule: { kind: "import_specifier" } }).length ?? 0; + if (importNode && specifierCount === 1) { + const alias = + actNamedImport.alias && actNamedImport.alias !== "act" + ? `act as ${actNamedImport.alias}` + : "act"; + const existingReactNamedImport = rootNode.findAll({ rule: { kind: "import_statement" } }).find((imp) => + imp.find({ rule: { kind: "string", has: { kind: "string_fragment", regex: "^react$" } } }) !== null && + imp.find({ rule: { kind: "named_imports" } }) !== null + ); + if (existingReactNamedImport) { + const namedImports = existingReactNamedImport.find({ rule: { kind: "named_imports" } }); + if (namedImports) { + const insertPos = namedImports.range().end.index - 1; + edits.push({ startPos: insertPos, endPos: insertPos, insertedText: `, ${alias}` }); + } + edits.push(importNode.replace("")); + } else { + edits.push(importNode.replace(`import { ${alias} } from "react";`)); - const removeEdit = removeImport(rootNode, { - type: "named", - specifiers: ["act"], - from: TEST_UTILS_MODULE, - }); - if (removeEdit) edits.push(removeEdit); - metric.increment({ file: root.filename(), pattern: "named-import" }); + } + } else { + const addEdit = addImport(rootNode, { + type: "named", + specifiers: [{ name: "act", alias: actNamedImport.alias }], + from: REACT_MODULE, + }); + if (addEdit) edits.push(addEdit); + + const removeEdit = removeImport(rootNode, { + type: "named", + specifiers: ["act"], + from: TEST_UTILS_MODULE, + }); + if (removeEdit) edits.push(removeEdit); + } + metric.increment({ file: metricFile(root.filename()), pattern: "named-import" }); } const exportDecls = rootNode.findAll({ @@ -140,7 +173,7 @@ const transform: Transform = async (root) => { const stringNode = exp.find({ rule: { kind: "string", regex: "react-dom/test-utils" } }); if (stringNode) { edits.push(stringNode.replace('"react"')); - metric.increment({ file: root.filename(), pattern: "re-export" }); + metric.increment({ file: metricFile(root.filename()), pattern: "re-export" }); } } else if (hasExportAll) { const insertPos = exp.range().end.index; @@ -149,7 +182,7 @@ const transform: Transform = async (root) => { endPos: insertPos, insertedText: "\nexport { act } from \"react\";", }); - metric.increment({ file: root.filename(), pattern: "re-export" }); + metric.increment({ file: metricFile(root.filename()), pattern: "re-export" }); } } diff --git a/replace-act-import/tests/default-import/expected.tsx b/codemods/replace-act-import/tests/default-import/expected.tsx similarity index 100% rename from replace-act-import/tests/default-import/expected.tsx rename to codemods/replace-act-import/tests/default-import/expected.tsx diff --git a/replace-act-import/tests/default-import/input.tsx b/codemods/replace-act-import/tests/default-import/input.tsx similarity index 100% rename from replace-act-import/tests/default-import/input.tsx rename to codemods/replace-act-import/tests/default-import/input.tsx diff --git a/replace-act-import/tests/default-import/metrics.json b/codemods/replace-act-import/tests/default-import/metrics.json similarity index 100% rename from replace-act-import/tests/default-import/metrics.json rename to codemods/replace-act-import/tests/default-import/metrics.json diff --git a/replace-act-import/tests/named-import-2/expected.tsx b/codemods/replace-act-import/tests/named-import-2/expected.tsx similarity index 97% rename from replace-act-import/tests/named-import-2/expected.tsx rename to codemods/replace-act-import/tests/named-import-2/expected.tsx index 75b5c0c..6a1ffe1 100644 --- a/replace-act-import/tests/named-import-2/expected.tsx +++ b/codemods/replace-act-import/tests/named-import-2/expected.tsx @@ -1,3 +1,4 @@ import { FC , act} from "react"; + act(); diff --git a/replace-act-import/tests/named-import-2/input.tsx b/codemods/replace-act-import/tests/named-import-2/input.tsx similarity index 100% rename from replace-act-import/tests/named-import-2/input.tsx rename to codemods/replace-act-import/tests/named-import-2/input.tsx diff --git a/replace-act-import/tests/named-import-2/metrics.json b/codemods/replace-act-import/tests/named-import-2/metrics.json similarity index 100% rename from replace-act-import/tests/named-import-2/metrics.json rename to codemods/replace-act-import/tests/named-import-2/metrics.json diff --git a/codemods/replace-act-import/tests/named-import/expected.tsx b/codemods/replace-act-import/tests/named-import/expected.tsx new file mode 100644 index 0000000..44ab638 --- /dev/null +++ b/codemods/replace-act-import/tests/named-import/expected.tsx @@ -0,0 +1,2 @@ +import { act } from "react"; +act(); diff --git a/replace-act-import/tests/named-import/input.tsx b/codemods/replace-act-import/tests/named-import/input.tsx similarity index 100% rename from replace-act-import/tests/named-import/input.tsx rename to codemods/replace-act-import/tests/named-import/input.tsx diff --git a/replace-act-import/tests/named-import/metrics.json b/codemods/replace-act-import/tests/named-import/metrics.json similarity index 100% rename from replace-act-import/tests/named-import/metrics.json rename to codemods/replace-act-import/tests/named-import/metrics.json diff --git a/replace-act-import/tests/other-import/expected.tsx b/codemods/replace-act-import/tests/other-import/expected.tsx similarity index 100% rename from replace-act-import/tests/other-import/expected.tsx rename to codemods/replace-act-import/tests/other-import/expected.tsx diff --git a/replace-act-import/tests/other-import/input.tsx b/codemods/replace-act-import/tests/other-import/input.tsx similarity index 100% rename from replace-act-import/tests/other-import/input.tsx rename to codemods/replace-act-import/tests/other-import/input.tsx diff --git a/replace-act-import/tests/re-export/expected.tsx b/codemods/replace-act-import/tests/re-export/expected.tsx similarity index 100% rename from replace-act-import/tests/re-export/expected.tsx rename to codemods/replace-act-import/tests/re-export/expected.tsx diff --git a/replace-act-import/tests/re-export/input.tsx b/codemods/replace-act-import/tests/re-export/input.tsx similarity index 100% rename from replace-act-import/tests/re-export/input.tsx rename to codemods/replace-act-import/tests/re-export/input.tsx diff --git a/replace-act-import/tests/re-export/metrics.json b/codemods/replace-act-import/tests/re-export/metrics.json similarity index 100% rename from replace-act-import/tests/re-export/metrics.json rename to codemods/replace-act-import/tests/re-export/metrics.json diff --git a/codemods/replace-act-import/tests/wildcard-import-2/expected.tsx b/codemods/replace-act-import/tests/wildcard-import-2/expected.tsx new file mode 100644 index 0000000..fc21df2 --- /dev/null +++ b/codemods/replace-act-import/tests/wildcard-import-2/expected.tsx @@ -0,0 +1,3 @@ +import * as React from "react"; + +React.act(); diff --git a/replace-act-import/tests/wildcard-import-2/input.tsx b/codemods/replace-act-import/tests/wildcard-import-2/input.tsx similarity index 100% rename from replace-act-import/tests/wildcard-import-2/input.tsx rename to codemods/replace-act-import/tests/wildcard-import-2/input.tsx diff --git a/replace-act-import/tests/wildcard-import-2/metrics.json b/codemods/replace-act-import/tests/wildcard-import-2/metrics.json similarity index 100% rename from replace-act-import/tests/wildcard-import-2/metrics.json rename to codemods/replace-act-import/tests/wildcard-import-2/metrics.json diff --git a/codemods/replace-act-import/tests/wildcard-import/expected.tsx b/codemods/replace-act-import/tests/wildcard-import/expected.tsx new file mode 100644 index 0000000..4acf1aa --- /dev/null +++ b/codemods/replace-act-import/tests/wildcard-import/expected.tsx @@ -0,0 +1,2 @@ +import * as React from "react"; +React.act(); diff --git a/replace-act-import/tests/wildcard-import/input.tsx b/codemods/replace-act-import/tests/wildcard-import/input.tsx similarity index 100% rename from replace-act-import/tests/wildcard-import/input.tsx rename to codemods/replace-act-import/tests/wildcard-import/input.tsx diff --git a/replace-act-import/tests/wildcard-import/metrics.json b/codemods/replace-act-import/tests/wildcard-import/metrics.json similarity index 100% rename from replace-act-import/tests/wildcard-import/metrics.json rename to codemods/replace-act-import/tests/wildcard-import/metrics.json diff --git a/codemods/replace-act-import/tsconfig.json b/codemods/replace-act-import/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/replace-act-import/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/replace-act-import/workflow.yaml b/codemods/replace-act-import/workflow.yaml similarity index 100% rename from replace-act-import/workflow.yaml rename to codemods/replace-act-import/workflow.yaml diff --git a/replace-reactdom-render/.gitignore b/codemods/replace-reactdom-render/.gitignore similarity index 100% rename from replace-reactdom-render/.gitignore rename to codemods/replace-reactdom-render/.gitignore diff --git a/codemods/replace-reactdom-render/README.md b/codemods/replace-reactdom-render/README.md new file mode 100644 index 0000000..c545d33 --- /dev/null +++ b/codemods/replace-reactdom-render/README.md @@ -0,0 +1,16 @@ +# @react/replace-reactdom-render + +Replace `ReactDOM.render` and named `render` imports with `createRoot(...).render(...)`. + +## Usage + +```bash +codemod run @react/replace-reactdom-render +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/replace-reactdom-render/codemod.yaml b/codemods/replace-reactdom-render/codemod.yaml similarity index 68% rename from replace-reactdom-render/codemod.yaml rename to codemods/replace-reactdom-render/codemod.yaml index f766db4..b2b3bcf 100644 --- a/replace-reactdom-render/codemod.yaml +++ b/codemods/replace-reactdom-render/codemod.yaml @@ -1,9 +1,9 @@ schema_version: "1.0" -name: "@react-codemods/replace-reactdom-render" +name: "@react/replace-reactdom-render" version: "0.1.0" description: "Replace ReactDOM.render with createRoot for React 18" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" @@ -11,7 +11,7 @@ workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration"] +keywords: ["React"] registry: access: "public" diff --git a/replace-reactdom-render/package.json b/codemods/replace-reactdom-render/package.json similarity index 78% rename from replace-reactdom-render/package.json rename to codemods/replace-reactdom-render/package.json index 2fb19f2..f6deff3 100644 --- a/replace-reactdom-render/package.json +++ b/codemods/replace-reactdom-render/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/replace-reactdom-render", + "name": "@react/replace-reactdom-render", "version": "0.1.0", "description": "Replace ReactDOM.render with createRoot for React 18", "type": "module", @@ -9,7 +9,8 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" }, "dependencies": { "@jssg/utils": "^0.0.2" diff --git a/replace-reactdom-render/pnpm-lock.yaml b/codemods/replace-reactdom-render/pnpm-lock.yaml similarity index 100% rename from replace-reactdom-render/pnpm-lock.yaml rename to codemods/replace-reactdom-render/pnpm-lock.yaml diff --git a/replace-reactdom-render/scripts/codemod.ts b/codemods/replace-reactdom-render/scripts/codemod.ts similarity index 81% rename from replace-reactdom-render/scripts/codemod.ts rename to codemods/replace-reactdom-render/scripts/codemod.ts index 2dddbf0..bd64e14 100644 --- a/replace-reactdom-render/scripts/codemod.ts +++ b/codemods/replace-reactdom-render/scripts/codemod.ts @@ -1,7 +1,12 @@ import type { Transform, Edit } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport, addImport, removeImport } from "@jssg/utils/javascript/imports"; +import { getImport } from "@jssg/utils/javascript/imports"; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} const transform: Transform = async (root) => { @@ -65,8 +70,8 @@ const transform: Transform = async (root) => { ); if (argList.length >= 2) { - const element = argList[0]; - const container = argList[1]; + const element = argList[0]!; + const container = argList[1]!; const statement = call.ancestors().find((ancestor) => ancestor.kind() === "expression_statement" @@ -86,12 +91,12 @@ const transform: Transform = async (root) => { indent = ""; } } - const replacement = `${indent}const ${rootName} = createRoot(${container.text()});\n${indent}${rootName}.render(${element.text()});`; + const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${rootName}.render(${element.text()});`; edits.push(statement.replace(replacement)); hasTransformations = true; transformMetric.increment({ pattern: "ReactDOM.render", - file: root.filename(), + file: metricFile(root.filename()), }); } } @@ -119,8 +124,8 @@ const transform: Transform = async (root) => { ); if (argList.length >= 2) { - const element = argList[0]; - const container = argList[1]; + const element = argList[0]!; + const container = argList[1]!; const statement = call.ancestors().find((ancestor) => ancestor.kind() === "expression_statement" @@ -140,12 +145,12 @@ const transform: Transform = async (root) => { indent = ""; } } - const replacement = `${indent}const ${rootName} = createRoot(${container.text()});\n${indent}${rootName}.render(${element.text()});`; + const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${rootName}.render(${element.text()});`; edits.push(statement.replace(replacement)); hasTransformations = true; transformMetric.increment({ pattern: "render", - file: root.filename(), + file: metricFile(root.filename()), }); } } @@ -156,13 +161,18 @@ const transform: Transform = async (root) => { return null; } - const addEdit = addImport(rootNode, { + const hasCreateRootImport = getImport(rootNode, { type: "named", - specifiers: [{ name: "createRoot" }], + name: "createRoot", from: "react-dom/client", }); - if (addEdit) { - edits.push(addEdit); + if (!hasCreateRootImport) { + edits.push({ + startPos: 0, + endPos: 0, + insertedText: 'import { createRoot } from "react-dom/client";\n', + + }); } return rootNode.commitEdits(edits); diff --git a/replace-reactdom-render/tests/original-default/expected.tsx b/codemods/replace-reactdom-render/tests/default/expected.tsx similarity index 59% rename from replace-reactdom-render/tests/original-default/expected.tsx rename to codemods/replace-reactdom-render/tests/default/expected.tsx index cb093d3..7a3e571 100644 --- a/replace-reactdom-render/tests/original-default/expected.tsx +++ b/codemods/replace-reactdom-render/tests/default/expected.tsx @@ -1,6 +1,6 @@ +import { createRoot } from "react-dom/client"; import ReactDom from "react-dom"; -import Component from "Component";import { createRoot } from 'react-dom/client'; - +import Component from "Component"; const root = createRoot(document.getElementById("app")); root.render(); diff --git a/replace-reactdom-render/tests/original-default/input.tsx b/codemods/replace-reactdom-render/tests/default/input.tsx similarity index 100% rename from replace-reactdom-render/tests/original-default/input.tsx rename to codemods/replace-reactdom-render/tests/default/input.tsx diff --git a/replace-reactdom-render/tests/reactdom-member/metrics.json b/codemods/replace-reactdom-render/tests/default/metrics.json similarity index 73% rename from replace-reactdom-render/tests/reactdom-member/metrics.json rename to codemods/replace-reactdom-render/tests/default/metrics.json index 2dc28f7..95d32e3 100644 --- a/replace-reactdom-render/tests/reactdom-member/metrics.json +++ b/codemods/replace-reactdom-render/tests/default/metrics.json @@ -2,7 +2,7 @@ "reactdom-render-replacements": [ { "cardinality": { - "file": "tests/reactdom-member/input.tsx", + "file": "tests/default/input.tsx", "pattern": "ReactDOM.render" }, "count": 1 diff --git a/codemods/replace-reactdom-render/tests/nested/expected.tsx b/codemods/replace-reactdom-render/tests/nested/expected.tsx new file mode 100644 index 0000000..4f659df --- /dev/null +++ b/codemods/replace-reactdom-render/tests/nested/expected.tsx @@ -0,0 +1,9 @@ +import { createRoot } from "react-dom/client"; +import { render } from "react-dom"; + +const fn = () => { + if (true) { + const root = createRoot(theNode); + root.render(); + } +}; diff --git a/replace-reactdom-render/tests/original-nested/input.tsx b/codemods/replace-reactdom-render/tests/nested/input.tsx similarity index 100% rename from replace-reactdom-render/tests/original-nested/input.tsx rename to codemods/replace-reactdom-render/tests/nested/input.tsx diff --git a/replace-reactdom-render/tests/named-import/metrics.json b/codemods/replace-reactdom-render/tests/nested/metrics.json similarity index 73% rename from replace-reactdom-render/tests/named-import/metrics.json rename to codemods/replace-reactdom-render/tests/nested/metrics.json index 49282a5..4d372e2 100644 --- a/replace-reactdom-render/tests/named-import/metrics.json +++ b/codemods/replace-reactdom-render/tests/nested/metrics.json @@ -2,7 +2,7 @@ "reactdom-render-replacements": [ { "cardinality": { - "file": "tests/named-import/input.tsx", + "file": "tests/nested/input.tsx", "pattern": "render" }, "count": 1 diff --git a/codemods/replace-reactdom-render/tsconfig.json b/codemods/replace-reactdom-render/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/replace-reactdom-render/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/react-proptypes-to-prop-types/workflow.yaml b/codemods/replace-reactdom-render/workflow.yaml similarity index 100% rename from react-proptypes-to-prop-types/workflow.yaml rename to codemods/replace-reactdom-render/workflow.yaml diff --git a/replace-string-ref/.gitignore b/codemods/replace-string-ref/.gitignore similarity index 100% rename from replace-string-ref/.gitignore rename to codemods/replace-string-ref/.gitignore diff --git a/codemods/replace-string-ref/README.md b/codemods/replace-string-ref/README.md new file mode 100644 index 0000000..a371e19 --- /dev/null +++ b/codemods/replace-string-ref/README.md @@ -0,0 +1,16 @@ +# @react/replace-string-ref + +Replace string refs in React class components with callback refs that assign through `this.refs`. + +## Usage + +```bash +codemod run @react/replace-string-ref +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/replace-string-ref/codemod.yaml b/codemods/replace-string-ref/codemod.yaml similarity index 70% rename from replace-string-ref/codemod.yaml rename to codemods/replace-string-ref/codemod.yaml index 1b89cdc..a28823c 100644 --- a/replace-string-ref/codemod.yaml +++ b/codemods/replace-string-ref/codemod.yaml @@ -1,9 +1,9 @@ schema_version: "1.0" -name: "@react-codemods/replace-string-ref" +name: "@react/replace-string-ref" version: "0.1.0" description: "Replace string refs with callback refs in React class components" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" @@ -11,7 +11,7 @@ workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration"] +keywords: ["React"] registry: access: "public" diff --git a/replace-string-ref/package.json b/codemods/replace-string-ref/package.json similarity index 80% rename from replace-string-ref/package.json rename to codemods/replace-string-ref/package.json index 015cc8d..19f3f37 100644 --- a/replace-string-ref/package.json +++ b/codemods/replace-string-ref/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/replace-string-ref", + "name": "@react/replace-string-ref", "version": "0.1.0", "description": "Replace string refs with callback refs in React class components", "type": "module", @@ -9,7 +9,8 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" }, "dependencies": { "@jssg/utils": "^0.0.2" diff --git a/replace-string-ref/pnpm-lock.yaml b/codemods/replace-string-ref/pnpm-lock.yaml similarity index 100% rename from replace-string-ref/pnpm-lock.yaml rename to codemods/replace-string-ref/pnpm-lock.yaml diff --git a/replace-string-ref/scripts/codemod.ts b/codemods/replace-string-ref/scripts/codemod.ts similarity index 77% rename from replace-string-ref/scripts/codemod.ts rename to codemods/replace-string-ref/scripts/codemod.ts index 260ab64..828493e 100644 --- a/replace-string-ref/scripts/codemod.ts +++ b/codemods/replace-string-ref/scripts/codemod.ts @@ -3,6 +3,12 @@ import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; import { getImport } from "@jssg/utils/javascript/imports"; +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + + function isInsideReactClassComponent( node: SgNode, reactName: string, @@ -55,6 +61,7 @@ const transform: Transform = async (root) => { const transformMetric = useMetricAtom("string-ref-replacements"); const refNames: string[] = []; + const wrappedJsxStarts = new Set(); const reactDefault = getImport(rootNode, { type: "default", from: "react" }); const componentImport = getImport(rootNode, { type: "named", name: "Component", from: "react" }); @@ -100,13 +107,35 @@ const transform: Transform = async (root) => { }}`; edits.push(refAttr.replace(callbackRef)); + const jsx = refAttr.ancestors().find((a) => + a.kind() === "jsx_self_closing_element" || a.kind() === "jsx_element" + ); + const returnStmt = refAttr.ancestors().find((a) => a.kind() === "return_statement"); + if ( + jsx && + returnStmt && + jsx.parent()?.kind() !== "parenthesized_expression" && + !wrappedJsxStarts.has(jsx.range().start.index) + ) { + wrappedJsxStarts.add(jsx.range().start.index); + edits.push({ + startPos: jsx.range().start.index, + endPos: jsx.range().start.index, + insertedText: "(", + }); + edits.push({ + startPos: jsx.range().end.index, + endPos: jsx.range().end.index, + insertedText: ")", + }); + } refNames.push(refName); } if (refNames.length > 0) { transformMetric.increment({ refs: refNames.sort().join(","), - file: root.filename(), + file: metricFile(root.filename()), }, refNames.length); } diff --git a/replace-string-ref/tests/original-class-component-custom-import-names/expected.tsx b/codemods/replace-string-ref/tests/class-component-custom-import-names/expected.tsx similarity index 71% rename from replace-string-ref/tests/original-class-component-custom-import-names/expected.tsx rename to codemods/replace-string-ref/tests/class-component-custom-import-names/expected.tsx index 9b5faed..70193fe 100644 --- a/replace-string-ref/tests/original-class-component-custom-import-names/expected.tsx +++ b/codemods/replace-string-ref/tests/class-component-custom-import-names/expected.tsx @@ -2,16 +2,16 @@ import React1, { PureComponent as PureComponent1 } from "react"; class C extends React1.Component { render() { - return
{ + return (
{ this.refs.refName = ref; - }} />; + }} />); } } class C1 extends PureComponent1 { render() { - return
{ + return (
{ this.refs.refName = ref; - }} />; + }} />); } } diff --git a/replace-string-ref/tests/original-class-component-custom-import-names/input.tsx b/codemods/replace-string-ref/tests/class-component-custom-import-names/input.tsx similarity index 100% rename from replace-string-ref/tests/original-class-component-custom-import-names/input.tsx rename to codemods/replace-string-ref/tests/class-component-custom-import-names/input.tsx diff --git a/replace-string-ref/tests/original-class-component-named-import/metrics.json b/codemods/replace-string-ref/tests/class-component-custom-import-names/metrics.json similarity index 64% rename from replace-string-ref/tests/original-class-component-named-import/metrics.json rename to codemods/replace-string-ref/tests/class-component-custom-import-names/metrics.json index 13664cb..9be800e 100644 --- a/replace-string-ref/tests/original-class-component-named-import/metrics.json +++ b/codemods/replace-string-ref/tests/class-component-custom-import-names/metrics.json @@ -2,7 +2,7 @@ "string-ref-replacements": [ { "cardinality": { - "file": "tests/original-class-component-named-import/input.tsx", + "file": "tests/class-component-custom-import-names/input.tsx", "refs": "refName,refName" }, "count": 2 diff --git a/replace-string-ref/tests/original-class-component-default-import/expected.tsx b/codemods/replace-string-ref/tests/class-component-default-import/expected.tsx similarity index 68% rename from replace-string-ref/tests/original-class-component-default-import/expected.tsx rename to codemods/replace-string-ref/tests/class-component-default-import/expected.tsx index e653027..90367dd 100644 --- a/replace-string-ref/tests/original-class-component-default-import/expected.tsx +++ b/codemods/replace-string-ref/tests/class-component-default-import/expected.tsx @@ -2,16 +2,16 @@ import React from "react"; class C extends React.Component { render() { - return
{ + return (
{ this.refs.refName = ref; - }} />; + }} />); } } class C1 extends React.PureComponent { render() { - return
{ + return (
{ this.refs.refName = ref; - }} />; + }} />); } } diff --git a/replace-string-ref/tests/original-class-component-default-import/input.tsx b/codemods/replace-string-ref/tests/class-component-default-import/input.tsx similarity index 100% rename from replace-string-ref/tests/original-class-component-default-import/input.tsx rename to codemods/replace-string-ref/tests/class-component-default-import/input.tsx diff --git a/replace-string-ref/tests/original-class-component-default-import/metrics.json b/codemods/replace-string-ref/tests/class-component-default-import/metrics.json similarity index 63% rename from replace-string-ref/tests/original-class-component-default-import/metrics.json rename to codemods/replace-string-ref/tests/class-component-default-import/metrics.json index 13a1bca..a7db8e3 100644 --- a/replace-string-ref/tests/original-class-component-default-import/metrics.json +++ b/codemods/replace-string-ref/tests/class-component-default-import/metrics.json @@ -2,7 +2,7 @@ "string-ref-replacements": [ { "cardinality": { - "file": "tests/original-class-component-default-import/input.tsx", + "file": "tests/class-component-default-import/input.tsx", "refs": "refName,refName" }, "count": 2 diff --git a/replace-string-ref/tests/original-class-component-named-import/expected.tsx b/codemods/replace-string-ref/tests/class-component-named-import/expected.tsx similarity index 69% rename from replace-string-ref/tests/original-class-component-named-import/expected.tsx rename to codemods/replace-string-ref/tests/class-component-named-import/expected.tsx index ab6052a..cac99e6 100644 --- a/replace-string-ref/tests/original-class-component-named-import/expected.tsx +++ b/codemods/replace-string-ref/tests/class-component-named-import/expected.tsx @@ -2,16 +2,16 @@ import { Component, PureComponent } from "react"; class C extends Component { render() { - return
{ + return (
{ this.refs.refName = ref; - }} />; + }} />); } } class C1 extends PureComponent { render() { - return
{ + return (
{ this.refs.refName = ref; - }} />; + }} />); } } diff --git a/replace-string-ref/tests/original-class-component-named-import/input.tsx b/codemods/replace-string-ref/tests/class-component-named-import/input.tsx similarity index 100% rename from replace-string-ref/tests/original-class-component-named-import/input.tsx rename to codemods/replace-string-ref/tests/class-component-named-import/input.tsx diff --git a/replace-string-ref/tests/original-class-component-custom-import-names/metrics.json b/codemods/replace-string-ref/tests/class-component-named-import/metrics.json similarity index 62% rename from replace-string-ref/tests/original-class-component-custom-import-names/metrics.json rename to codemods/replace-string-ref/tests/class-component-named-import/metrics.json index 979d65e..1ca0b5f 100644 --- a/replace-string-ref/tests/original-class-component-custom-import-names/metrics.json +++ b/codemods/replace-string-ref/tests/class-component-named-import/metrics.json @@ -2,7 +2,7 @@ "string-ref-replacements": [ { "cardinality": { - "file": "tests/original-class-component-custom-import-names/input.tsx", + "file": "tests/class-component-named-import/input.tsx", "refs": "refName,refName" }, "count": 2 diff --git a/replace-string-ref/tests/original-function-component/expected.tsx b/codemods/replace-string-ref/tests/function-component/expected.tsx similarity index 100% rename from replace-string-ref/tests/original-function-component/expected.tsx rename to codemods/replace-string-ref/tests/function-component/expected.tsx diff --git a/replace-string-ref/tests/original-function-component/input.tsx b/codemods/replace-string-ref/tests/function-component/input.tsx similarity index 100% rename from replace-string-ref/tests/original-function-component/input.tsx rename to codemods/replace-string-ref/tests/function-component/input.tsx diff --git a/codemods/replace-string-ref/tsconfig.json b/codemods/replace-string-ref/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/replace-string-ref/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/rename-unsafe-lifecycles/workflow.yaml b/codemods/replace-string-ref/workflow.yaml similarity index 100% rename from rename-unsafe-lifecycles/workflow.yaml rename to codemods/replace-string-ref/workflow.yaml diff --git a/codemods/replace-use-form-state/README.md b/codemods/replace-use-form-state/README.md new file mode 100644 index 0000000..9b9ec51 --- /dev/null +++ b/codemods/replace-use-form-state/README.md @@ -0,0 +1,16 @@ +# @react/replace-use-form-state + +Rename `useFormState` to `useActionState` in `react-dom` imports and usages. + +## Usage + +```bash +codemod run @react/replace-use-form-state +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/replace-use-form-state/codemod.yaml b/codemods/replace-use-form-state/codemod.yaml similarity index 63% rename from replace-use-form-state/codemod.yaml rename to codemods/replace-use-form-state/codemod.yaml index 23dc871..3b2eea4 100644 --- a/replace-use-form-state/codemod.yaml +++ b/codemods/replace-use-form-state/codemod.yaml @@ -1,16 +1,16 @@ schema_version: "1.0" -name: "@react-codemods/replace-use-form-state" +name: "@react/replace-use-form-state" version: "0.1.0" description: "Rename useFormState to useActionState in react-dom imports and usages" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration", "react", "useFormState", "useActionState"] +keywords: ["React"] registry: access: "public" diff --git a/replace-use-form-state/package.json b/codemods/replace-use-form-state/package.json similarity index 79% rename from replace-use-form-state/package.json rename to codemods/replace-use-form-state/package.json index 94f3c83..34da452 100644 --- a/replace-use-form-state/package.json +++ b/codemods/replace-use-form-state/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/replace-use-form-state", + "name": "@react/replace-use-form-state", "version": "0.1.0", "description": "Rename useFormState to useActionState in react-dom imports and usages", "type": "module", @@ -9,7 +9,8 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" }, "dependencies": { "@jssg/utils": "^0.0.2" diff --git a/replace-use-form-state/scripts/codemod.ts b/codemods/replace-use-form-state/scripts/codemod.ts similarity index 88% rename from replace-use-form-state/scripts/codemod.ts rename to codemods/replace-use-form-state/scripts/codemod.ts index b43815d..e499a6a 100644 --- a/replace-use-form-state/scripts/codemod.ts +++ b/codemods/replace-use-form-state/scripts/codemod.ts @@ -3,11 +3,17 @@ import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; import { getImport } from "@jssg/utils/javascript/imports"; +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + + const REACT_DOM_MODULE = "react-dom"; type ReactDOMImport = { name: string; type: "default" | "namespace" }; -function getReactDOMImport(rootNode: SgNode): ReactDOMImport | null { +function getReactDOMImport(rootNode: SgNode): ReactDOMImport | null { const defaultImp = getImport(rootNode, { type: "default", from: REACT_DOM_MODULE }); if (defaultImp) return { name: defaultImp.alias, type: "default" }; @@ -62,7 +68,7 @@ const transform: Transform = async (root) => { const propNode = member.field("property"); if (propNode) { edits.push(propNode.replace("useActionState")); - metric.increment({ file: root.filename(), pattern: "member-call" }); + metric.increment({ file: metricFile(root.filename()), pattern: "member-call" }); } } } @@ -103,7 +109,7 @@ const transform: Transform = async (root) => { } } } - metric.increment({ file: root.filename(), pattern: "named-import" }); + metric.increment({ file: metricFile(root.filename()), pattern: "named-import" }); } if (edits.length === 0) return null; diff --git a/replace-use-form-state/tests/default-import/expected.tsx b/codemods/replace-use-form-state/tests/default-import/expected.tsx similarity index 100% rename from replace-use-form-state/tests/default-import/expected.tsx rename to codemods/replace-use-form-state/tests/default-import/expected.tsx diff --git a/replace-use-form-state/tests/default-import/input.tsx b/codemods/replace-use-form-state/tests/default-import/input.tsx similarity index 100% rename from replace-use-form-state/tests/default-import/input.tsx rename to codemods/replace-use-form-state/tests/default-import/input.tsx diff --git a/replace-use-form-state/tests/default-import/metrics.json b/codemods/replace-use-form-state/tests/default-import/metrics.json similarity index 100% rename from replace-use-form-state/tests/default-import/metrics.json rename to codemods/replace-use-form-state/tests/default-import/metrics.json diff --git a/replace-use-form-state/tests/named-import-2/expected.tsx b/codemods/replace-use-form-state/tests/named-import-2/expected.tsx similarity index 100% rename from replace-use-form-state/tests/named-import-2/expected.tsx rename to codemods/replace-use-form-state/tests/named-import-2/expected.tsx diff --git a/replace-use-form-state/tests/named-import-2/input.tsx b/codemods/replace-use-form-state/tests/named-import-2/input.tsx similarity index 100% rename from replace-use-form-state/tests/named-import-2/input.tsx rename to codemods/replace-use-form-state/tests/named-import-2/input.tsx diff --git a/replace-use-form-state/tests/named-import-2/metrics.json b/codemods/replace-use-form-state/tests/named-import-2/metrics.json similarity index 100% rename from replace-use-form-state/tests/named-import-2/metrics.json rename to codemods/replace-use-form-state/tests/named-import-2/metrics.json diff --git a/replace-use-form-state/tests/named-import-3/expected.tsx b/codemods/replace-use-form-state/tests/named-import-3/expected.tsx similarity index 100% rename from replace-use-form-state/tests/named-import-3/expected.tsx rename to codemods/replace-use-form-state/tests/named-import-3/expected.tsx diff --git a/replace-use-form-state/tests/named-import-3/input.tsx b/codemods/replace-use-form-state/tests/named-import-3/input.tsx similarity index 100% rename from replace-use-form-state/tests/named-import-3/input.tsx rename to codemods/replace-use-form-state/tests/named-import-3/input.tsx diff --git a/replace-use-form-state/tests/named-import-3/metrics.json b/codemods/replace-use-form-state/tests/named-import-3/metrics.json similarity index 100% rename from replace-use-form-state/tests/named-import-3/metrics.json rename to codemods/replace-use-form-state/tests/named-import-3/metrics.json diff --git a/replace-use-form-state/tests/named-import/expected.tsx b/codemods/replace-use-form-state/tests/named-import/expected.tsx similarity index 100% rename from replace-use-form-state/tests/named-import/expected.tsx rename to codemods/replace-use-form-state/tests/named-import/expected.tsx diff --git a/replace-use-form-state/tests/named-import/input.tsx b/codemods/replace-use-form-state/tests/named-import/input.tsx similarity index 100% rename from replace-use-form-state/tests/named-import/input.tsx rename to codemods/replace-use-form-state/tests/named-import/input.tsx diff --git a/replace-use-form-state/tests/named-import/metrics.json b/codemods/replace-use-form-state/tests/named-import/metrics.json similarity index 100% rename from replace-use-form-state/tests/named-import/metrics.json rename to codemods/replace-use-form-state/tests/named-import/metrics.json diff --git a/replace-use-form-state/tests/other-import/expected.tsx b/codemods/replace-use-form-state/tests/other-import/expected.tsx similarity index 100% rename from replace-use-form-state/tests/other-import/expected.tsx rename to codemods/replace-use-form-state/tests/other-import/expected.tsx diff --git a/replace-use-form-state/tests/other-import/input.tsx b/codemods/replace-use-form-state/tests/other-import/input.tsx similarity index 100% rename from replace-use-form-state/tests/other-import/input.tsx rename to codemods/replace-use-form-state/tests/other-import/input.tsx diff --git a/replace-use-form-state/tests/wildcard-import/expected.tsx b/codemods/replace-use-form-state/tests/wildcard-import/expected.tsx similarity index 100% rename from replace-use-form-state/tests/wildcard-import/expected.tsx rename to codemods/replace-use-form-state/tests/wildcard-import/expected.tsx diff --git a/replace-use-form-state/tests/wildcard-import/input.tsx b/codemods/replace-use-form-state/tests/wildcard-import/input.tsx similarity index 100% rename from replace-use-form-state/tests/wildcard-import/input.tsx rename to codemods/replace-use-form-state/tests/wildcard-import/input.tsx diff --git a/replace-use-form-state/tests/wildcard-import/metrics.json b/codemods/replace-use-form-state/tests/wildcard-import/metrics.json similarity index 100% rename from replace-use-form-state/tests/wildcard-import/metrics.json rename to codemods/replace-use-form-state/tests/wildcard-import/metrics.json diff --git a/codemods/replace-use-form-state/tsconfig.json b/codemods/replace-use-form-state/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/replace-use-form-state/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/replace-use-form-state/workflow.yaml b/codemods/replace-use-form-state/workflow.yaml similarity index 100% rename from replace-use-form-state/workflow.yaml rename to codemods/replace-use-form-state/workflow.yaml diff --git a/codemods/sort-comp/README.md b/codemods/sort-comp/README.md new file mode 100644 index 0000000..1920cad --- /dev/null +++ b/codemods/sort-comp/README.md @@ -0,0 +1,20 @@ +# @react/sort-comp + +Reorder React component members to match the `react/sort-comp` rule across `createClass` components and class components. + +## Usage + +```bash +codemod run @react/sort-comp +``` + +## Options + +- `methodsOrder`: ordered list of method groups and names used for sorting. Defaults to the built-in React sort order used by this package. + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/sort-comp/codemod.yaml b/codemods/sort-comp/codemod.yaml similarity index 69% rename from sort-comp/codemod.yaml rename to codemods/sort-comp/codemod.yaml index 1931761..673f8c4 100644 --- a/sort-comp/codemod.yaml +++ b/codemods/sort-comp/codemod.yaml @@ -1,16 +1,16 @@ schema_version: "1.0" -name: "@react-codemods/sort-comp" +name: "@react/sort-comp" version: "0.1.0" description: "Reorder React component methods to match eslint-plugin-react sort-comp rule" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "sort", "react", "sort-comp"] +keywords: ["React"] registry: access: "public" diff --git a/sort-comp/package.json b/codemods/sort-comp/package.json similarity index 79% rename from sort-comp/package.json rename to codemods/sort-comp/package.json index b12be0d..a906c44 100644 --- a/sort-comp/package.json +++ b/codemods/sort-comp/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/sort-comp", + "name": "@react/sort-comp", "version": "0.1.0", "description": "Reorder React component methods to match eslint-plugin-react sort-comp rule", "type": "module", @@ -9,6 +9,7 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" } } diff --git a/codemods/sort-comp/scripts/codemod.ts b/codemods/sort-comp/scripts/codemod.ts new file mode 100644 index 0000000..44abfd3 --- /dev/null +++ b/codemods/sort-comp/scripts/codemod.ts @@ -0,0 +1,200 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + + +type MemberChunk = { + node: SgNode; + name: string; + text: string; + start: number; + end: number; + originalIndex: number; +}; + +const DEFAULT_METHODS_ORDER = [ + "static-methods", + "displayName", + "propTypes", + "contextTypes", + "childContextTypes", + "mixins", + "statics", + "defaultProps", + "constructor", + "getDefaultProps", + "state", + "getInitialState", + "getChildContext", + "getDerivedStateFromProps", + "componentWillMount", + "UNSAFE_componentWillMount", + "componentDidMount", + "componentWillReceiveProps", + "UNSAFE_componentWillReceiveProps", + "shouldComponentUpdate", + "componentWillUpdate", + "UNSAFE_componentWillUpdate", + "getSnapshotBeforeUpdate", + "componentDidUpdate", + "componentDidCatch", + "componentWillUnmount", + "/^on.+$/", + "/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/", + "everything-else", + "/^render.+$/", + "render", +]; + +function selectorMatches(selector: string, name: string, isStatic: boolean): boolean { + if (selector === "static-methods") return isStatic; + if (selector === name) return true; + if (selector.startsWith("/") && selector.lastIndexOf("/") > 0) { + const lastSlash = selector.lastIndexOf("/"); + const body = selector.slice(1, lastSlash); + const flags = selector.slice(lastSlash + 1); + try { + return new RegExp(body, flags).test(name); + } catch { + return false; + } + } + return false; +} + +function correctIndex(order: string[], chunk: MemberChunk): number { + const everythingElse = order.indexOf("everything-else"); + const isStatic = chunk.node.text().startsWith("static "); + for (let i = 0; i < order.length; i++) { + if (i !== everythingElse && selectorMatches(order[i]!, chunk.name, isStatic)) return i; + } + return everythingElse >= 0 ? everythingElse : Number.POSITIVE_INFINITY; +} + +function memberName(node: SgNode): string { + if (node.kind() === "method_definition" || node.kind() === "public_field_definition") { + return node.field("name")?.text() ?? ""; + } + return node.field("key")?.text() ?? ""; +} + +function significantChildren(container: SgNode): SgNode[] { + return container.children() + .filter((child) => + child.kind() === "comment" || + child.kind() === "pair" || + child.kind() === "method_definition" || + child.kind() === "public_field_definition" + ) + .sort((a, b) => a.range().start.index - b.range().start.index); +} + +function collectChunks(container: SgNode, source: string): MemberChunk[] { + const chunks: MemberChunk[] = []; + let leadingStart: number | null = null; + + for (const child of significantChildren(container)) { + if (child.kind() === "comment") { + if (leadingStart === null) leadingStart = child.range().start.index; + continue; + } + const start = leadingStart ?? child.range().start.index; + const end = child.range().end.index; + chunks.push({ + node: child, + name: memberName(child), + text: source.slice(start, end).trim(), + start, + end, + originalIndex: chunks.length, + }); + leadingStart = null; + } + + return chunks; +} + +function sortChunks(chunks: MemberChunk[], order: string[]): MemberChunk[] { + return [...chunks].sort((a, b) => { + const indexA = correctIndex(order, a); + const indexB = correctIndex(order, b); + if (indexA !== indexB) return indexA - indexB; + const nameCompare = a.name.localeCompare(b.name); + return nameCompare !== 0 ? nameCompare : a.originalIndex - b.originalIndex; + }); +} + +function createClassConfig(call: SgNode): SgNode | null { + const args = call.field("arguments"); + if (!args) return null; + return args.children().find((child) => child.kind() === "object") ?? null; +} + +function isCreateClassCall(call: SgNode): boolean { + const fn = call.field("function"); + if (!fn) return false; + if (fn.kind() === "identifier") return fn.text() === "createClass"; + return fn.kind() === "member_expression" && fn.field("property")?.text() === "createClass"; +} + +function isReactClass(node: SgNode): boolean { + const heritage = node.find({ rule: { kind: "class_heritage" } }); + if (!heritage) return false; + return heritage.findAll({ rule: { kind: "identifier" } }) + .some((id) => id.text() === "Component" || id.text() === "PureComponent" || id.text() === "React"); +} + +function replacementFor(container: SgNode, chunks: MemberChunk[], order: string[], sep: string): Edit | null { + if (chunks.length <= 1) return null; + const sorted = sortChunks(chunks, order); + const changed = sorted.some((chunk, index) => chunk.originalIndex !== index); + if (!changed) return null; + return { + startPos: chunks[0]!.start, + endPos: chunks[chunks.length - 1]!.end, + insertedText: sorted.map((chunk) => chunk.text).join(sep), + }; +} + +const transform: Transform = async (root, options) => { + const rootNode = root.root(); + const source = rootNode.text(); + const edits: Edit[] = []; + const metric = useMetricAtom("sort-comp-reorders"); + const methodsOrderParam = options.params?.methodsOrder as unknown; + const methodsOrder = Array.isArray(methodsOrderParam) + ? methodsOrderParam.filter((item): item is string => typeof item === "string") + : DEFAULT_METHODS_ORDER; + + for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { + if (!isCreateClassCall(call)) continue; + const config = createClassConfig(call); + if (!config) continue; + const edit = replacementFor(config, collectChunks(config, source), methodsOrder, ",\n\n "); + if (edit) { + edits.push(edit); + metric.increment({ file: metricFile(root.filename()), kind: "createClass" }); + } + } + + for (const classDecl of rootNode.findAll({ rule: { kind: "class_declaration" } })) { + if (!isReactClass(classDecl)) continue; + const body = classDecl.find({ rule: { kind: "class_body" } }); + if (!body) continue; + const edit = replacementFor(body, collectChunks(body, source), methodsOrder, "\n\n "); + if (edit) { + edits.push(edit); + metric.increment({ file: metricFile(root.filename()), kind: "class" }); + } + } + + if (edits.length === 0) return null; + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/sort-comp/tests/custom-sort-custom-sort/expected.tsx b/codemods/sort-comp/tests/custom-sort-custom-sort/expected.tsx new file mode 100644 index 0000000..7822c3f --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort-custom-sort/expected.tsx @@ -0,0 +1,21 @@ +var React = require('react/addons'); + +// comment above createClass +var MyComponent = React.createClass({ + // comment on componentDidMount + componentDidMount() {}, + + mixins: [PureRenderMixin], + + myOwnMethod(foo) { + // comment within method + }, + + render: function() { + return
; + }, + +}); + +/* comment at end */ +module.exports = MyComponent; diff --git a/codemods/sort-comp/tests/custom-sort-custom-sort/input.tsx b/codemods/sort-comp/tests/custom-sort-custom-sort/input.tsx new file mode 100644 index 0000000..ad0764d --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort-custom-sort/input.tsx @@ -0,0 +1,21 @@ +var React = require('react/addons'); + +// comment above createClass +var MyComponent = React.createClass({ + render: function() { + return
; + }, + + mixins: [PureRenderMixin], + + // comment on componentDidMount + componentDidMount() {}, + + myOwnMethod(foo) { + // comment within method + }, + +}); + +/* comment at end */ +module.exports = MyComponent; diff --git a/codemods/sort-comp/tests/custom-sort-custom-sort/metrics.json b/codemods/sort-comp/tests/custom-sort-custom-sort/metrics.json new file mode 100644 index 0000000..0149839 --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort-custom-sort/metrics.json @@ -0,0 +1,11 @@ +{ + "sort-comp-reorders": [ + { + "cardinality": { + "file": "tests/custom-sort-custom-sort/input.tsx", + "kind": "createClass" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/sort-comp/tests/custom-sort-custom-sort/test.config.json b/codemods/sort-comp/tests/custom-sort-custom-sort/test.config.json new file mode 100644 index 0000000..73a66bb --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort-custom-sort/test.config.json @@ -0,0 +1,10 @@ +{ + "params": { + "methodsOrder": [ + "componentDidMount", + "mixins", + "everything-else", + "render" + ] + } +} diff --git a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/expected.tsx b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/expected.tsx new file mode 100644 index 0000000..9147794 --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/expected.tsx @@ -0,0 +1,24 @@ +var React = require('react/addons'); + +// comment above createClass +var MyComponent = React.createClass({ + // comment on componentDidMount + componentDidMount() {}, + + mixins: [PureRenderMixin], + + myOwnMethod(foo) { + // comment within method + }, + + renderChild: function() { + return
; + }, + + render: function() { + return renderChild(); + }, +}); + +/* comment at end */ +module.exports = MyComponent; diff --git a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/input.tsx b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/input.tsx new file mode 100644 index 0000000..d405432 --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/input.tsx @@ -0,0 +1,24 @@ +var React = require('react/addons'); + +// comment above createClass +var MyComponent = React.createClass({ + render: function() { + return renderChild(); + }, + + mixins: [PureRenderMixin], + + // comment on componentDidMount + componentDidMount() {}, + + myOwnMethod(foo) { + // comment within method + }, + + renderChild: function() { + return
; + }, +}); + +/* comment at end */ +module.exports = MyComponent; diff --git a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/metrics.json b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/metrics.json new file mode 100644 index 0000000..03cf5fc --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/metrics.json @@ -0,0 +1,11 @@ +{ + "sort-comp-reorders": [ + { + "cardinality": { + "file": "tests/custom-sort-group-custom-sort-group/input.tsx", + "kind": "createClass" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/test.config.json b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/test.config.json new file mode 100644 index 0000000..6609ce6 --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/test.config.json @@ -0,0 +1,11 @@ +{ + "params": { + "methodsOrder": [ + "componentDidMount", + "mixins", + "everything-else", + "/^render.+$/", + "render" + ] + } +} diff --git a/codemods/sort-comp/tests/sort-comp-pure/expected.tsx b/codemods/sort-comp/tests/sort-comp-pure/expected.tsx new file mode 100644 index 0000000..7b01fba --- /dev/null +++ b/codemods/sort-comp/tests/sort-comp-pure/expected.tsx @@ -0,0 +1,43 @@ +import React, { PureComponent } from 'react/addons'; + +const propTypes = {}; + +// comment above class +class MyPureComponent extends PureComponent { + static aStaticThing() { + // should come first + } + + static someStaticThing() { + // should bundle with other statics + } + + // comment on componentDidMount + componentDidMount() { + } + + myOwnMethod(foo) { + // comment within method + } + + renderBar() { + // should come before renderFoo + } + + renderFoo() { + // other render* function + } + + // comment at top of createClass + // this will be attached to first method + + render() { + return
; + } + +} + +MyPureComponent.propTypes = propTypes; + +/* comment at end */ +module.exports = MyPureComponent; diff --git a/codemods/sort-comp/tests/sort-comp-pure/input.tsx b/codemods/sort-comp/tests/sort-comp-pure/input.tsx new file mode 100644 index 0000000..963c4da --- /dev/null +++ b/codemods/sort-comp/tests/sort-comp-pure/input.tsx @@ -0,0 +1,43 @@ +import React, { PureComponent } from 'react/addons'; + +const propTypes = {}; + +// comment above class +class MyPureComponent extends PureComponent { + // comment at top of createClass + // this will be attached to first method + + render() { + return
; + } + + // comment on componentDidMount + componentDidMount() { + } + + static someStaticThing() { + // should bundle with other statics + } + + renderFoo() { + // other render* function + } + + renderBar() { + // should come before renderFoo + } + + static aStaticThing() { + // should come first + } + + myOwnMethod(foo) { + // comment within method + } + +} + +MyPureComponent.propTypes = propTypes; + +/* comment at end */ +module.exports = MyPureComponent; diff --git a/sort-comp/tests/original-createClass/metrics.json b/codemods/sort-comp/tests/sort-comp-pure/metrics.json similarity index 55% rename from sort-comp/tests/original-createClass/metrics.json rename to codemods/sort-comp/tests/sort-comp-pure/metrics.json index 76ffc6e..b40335c 100644 --- a/sort-comp/tests/original-createClass/metrics.json +++ b/codemods/sort-comp/tests/sort-comp-pure/metrics.json @@ -2,7 +2,8 @@ "sort-comp-reorders": [ { "cardinality": { - "file": "tests/original-createClass/input.tsx" + "file": "tests/sort-comp-pure/input.tsx", + "kind": "class" }, "count": 1 } diff --git a/sort-comp/tests/original-createClass/expected.tsx b/codemods/sort-comp/tests/sort-comp/expected.tsx similarity index 94% rename from sort-comp/tests/original-createClass/expected.tsx rename to codemods/sort-comp/tests/sort-comp/expected.tsx index e2b2210..7cbc621 100644 --- a/sort-comp/tests/original-createClass/expected.tsx +++ b/codemods/sort-comp/tests/sort-comp/expected.tsx @@ -2,15 +2,13 @@ var React = require('react/addons'); // comment above createClass var MyComponent = React.createClass({ - // comment at top of createClass - // this will be attached to first method - propTypes: { foo: bar, // comment on prop }, mixins: [PureRenderMixin], + // comment on componentDidMount componentDidMount() { }, @@ -26,6 +24,9 @@ var MyComponent = React.createClass({ // other render* function }, + // comment at top of createClass + // this will be attached to first method + render: function() { return
; }, diff --git a/sort-comp/tests/original-createClass/input.tsx b/codemods/sort-comp/tests/sort-comp/input.tsx similarity index 100% rename from sort-comp/tests/original-createClass/input.tsx rename to codemods/sort-comp/tests/sort-comp/input.tsx diff --git a/sort-comp/tests/real-world-container/metrics.json b/codemods/sort-comp/tests/sort-comp/metrics.json similarity index 55% rename from sort-comp/tests/real-world-container/metrics.json rename to codemods/sort-comp/tests/sort-comp/metrics.json index de9c9b2..9f8d4e0 100644 --- a/sort-comp/tests/real-world-container/metrics.json +++ b/codemods/sort-comp/tests/sort-comp/metrics.json @@ -2,7 +2,8 @@ "sort-comp-reorders": [ { "cardinality": { - "file": "tests/real-world-container/input.tsx" + "file": "tests/sort-comp/input.tsx", + "kind": "createClass" }, "count": 1 } diff --git a/codemods/sort-comp/tests/sort-comp2/expected.tsx b/codemods/sort-comp/tests/sort-comp2/expected.tsx new file mode 100644 index 0000000..71052ad --- /dev/null +++ b/codemods/sort-comp/tests/sort-comp2/expected.tsx @@ -0,0 +1,43 @@ +var React = require('react/addons'); + +const propTypes = {}; + +// comment above class +class MyComponent extends React.Component { + static aStaticThing() { + // should come first + } + + static someStaticThing() { + // should bundle with other statics + } + + // comment on componentDidMount + componentDidMount() { + } + + myOwnMethod(foo) { + // comment within method + } + + renderBar() { + // should come before renderFoo + } + + renderFoo() { + // other render* function + } + + // comment at top of createClass + // this will be attached to first method + + render() { + return
; + } + +} + +MyComponent.propTypes = propTypes; + +/* comment at end */ +module.exports = MyComponent; diff --git a/codemods/sort-comp/tests/sort-comp2/input.tsx b/codemods/sort-comp/tests/sort-comp2/input.tsx new file mode 100644 index 0000000..5d14a14 --- /dev/null +++ b/codemods/sort-comp/tests/sort-comp2/input.tsx @@ -0,0 +1,43 @@ +var React = require('react/addons'); + +const propTypes = {}; + +// comment above class +class MyComponent extends React.Component { + // comment at top of createClass + // this will be attached to first method + + render() { + return
; + } + + // comment on componentDidMount + componentDidMount() { + } + + static someStaticThing() { + // should bundle with other statics + } + + renderFoo() { + // other render* function + } + + renderBar() { + // should come before renderFoo + } + + static aStaticThing() { + // should come first + } + + myOwnMethod(foo) { + // comment within method + } + +} + +MyComponent.propTypes = propTypes; + +/* comment at end */ +module.exports = MyComponent; diff --git a/sort-comp/tests/lifecycle-order/metrics.json b/codemods/sort-comp/tests/sort-comp2/metrics.json similarity index 57% rename from sort-comp/tests/lifecycle-order/metrics.json rename to codemods/sort-comp/tests/sort-comp2/metrics.json index 8636725..2d17b75 100644 --- a/sort-comp/tests/lifecycle-order/metrics.json +++ b/codemods/sort-comp/tests/sort-comp2/metrics.json @@ -2,7 +2,8 @@ "sort-comp-reorders": [ { "cardinality": { - "file": "tests/lifecycle-order/input.tsx" + "file": "tests/sort-comp2/input.tsx", + "kind": "class" }, "count": 1 } diff --git a/codemods/sort-comp/tests/sort-comp3/expected.tsx b/codemods/sort-comp/tests/sort-comp3/expected.tsx new file mode 100644 index 0000000..a5bd4ca --- /dev/null +++ b/codemods/sort-comp/tests/sort-comp3/expected.tsx @@ -0,0 +1,43 @@ +import React, { Component } from 'react/addons'; + +const propTypes = {}; + +// comment above class +class MyComponent extends Component { + static aStaticThing() { + // should come first + } + + static someStaticThing() { + // should bundle with other statics + } + + // comment on componentDidMount + componentDidMount() { + } + + myOwnMethod(foo) { + // comment within method + } + + renderBar() { + // should come before renderFoo + } + + renderFoo() { + // other render* function + } + + // comment at top of createClass + // this will be attached to first method + + render() { + return
; + } + +} + +MyComponent.propTypes = propTypes; + +/* comment at end */ +module.exports = MyComponent; diff --git a/codemods/sort-comp/tests/sort-comp3/input.tsx b/codemods/sort-comp/tests/sort-comp3/input.tsx new file mode 100644 index 0000000..76dcb63 --- /dev/null +++ b/codemods/sort-comp/tests/sort-comp3/input.tsx @@ -0,0 +1,43 @@ +import React, { Component } from 'react/addons'; + +const propTypes = {}; + +// comment above class +class MyComponent extends Component { + // comment at top of createClass + // this will be attached to first method + + render() { + return
; + } + + // comment on componentDidMount + componentDidMount() { + } + + static someStaticThing() { + // should bundle with other statics + } + + renderFoo() { + // other render* function + } + + renderBar() { + // should come before renderFoo + } + + static aStaticThing() { + // should come first + } + + myOwnMethod(foo) { + // comment within method + } + +} + +MyComponent.propTypes = propTypes; + +/* comment at end */ +module.exports = MyComponent; diff --git a/sort-comp/tests/no-reorder-needed/metrics.json b/codemods/sort-comp/tests/sort-comp3/metrics.json similarity index 57% rename from sort-comp/tests/no-reorder-needed/metrics.json rename to codemods/sort-comp/tests/sort-comp3/metrics.json index 914f1ab..ea921ea 100644 --- a/sort-comp/tests/no-reorder-needed/metrics.json +++ b/codemods/sort-comp/tests/sort-comp3/metrics.json @@ -2,7 +2,8 @@ "sort-comp-reorders": [ { "cardinality": { - "file": "tests/no-reorder-needed/input.tsx" + "file": "tests/sort-comp3/input.tsx", + "kind": "class" }, "count": 1 } diff --git a/sort-comp/tsconfig.json b/codemods/sort-comp/tsconfig.json similarity index 51% rename from sort-comp/tsconfig.json rename to codemods/sort-comp/tsconfig.json index 321c0d7..ed30c5e 100644 --- a/sort-comp/tsconfig.json +++ b/codemods/sort-comp/tsconfig.json @@ -1,14 +1,24 @@ { "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], "module": "NodeNext", "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], + "types": [ + "@codemod.com/jssg-types", + "node" + ], "allowImportingTsExtensions": true, "noEmit": true, - "verbatimModuleSyntax": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, "erasableSyntaxOnly": true, "strict": true, "strictNullChecks": true }, - "exclude": ["tests"] + "exclude": [ + "tests" + ] } diff --git a/update-react-imports/workflow.yaml b/codemods/sort-comp/workflow.yaml similarity index 64% rename from update-react-imports/workflow.yaml rename to codemods/sort-comp/workflow.yaml index 8773841..a80a53d 100644 --- a/update-react-imports/workflow.yaml +++ b/codemods/sort-comp/workflow.yaml @@ -2,6 +2,15 @@ version: "1" +params: + schema: + methodsOrder: + name: "Methods Order" + description: "Ordered list of method groups and member names used for sorting." + type: array + items: + type: string + nodes: - id: apply-transforms name: Apply AST Transformations diff --git a/update-react-imports/.gitignore b/codemods/update-react-imports/.gitignore similarity index 100% rename from update-react-imports/.gitignore rename to codemods/update-react-imports/.gitignore diff --git a/codemods/update-react-imports/README.md b/codemods/update-react-imports/README.md new file mode 100644 index 0000000..ce21ed8 --- /dev/null +++ b/codemods/update-react-imports/README.md @@ -0,0 +1,20 @@ +# @react/update-react-imports + +Remove unnecessary React imports, convert member access to named imports when safe, and keep namespace imports when `React` is used as a value or type namespace. + +## Usage + +```bash +codemod run @react/update-react-imports +``` + +## Options + +- `destructureNamespaceImports`: allow namespace imports such as `import * as React` to be destructured into named imports when it is safe to do so. + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/update-react-imports/codemod.yaml b/codemods/update-react-imports/codemod.yaml similarity index 70% rename from update-react-imports/codemod.yaml rename to codemods/update-react-imports/codemod.yaml index a4626a6..a7eaeb1 100644 --- a/update-react-imports/codemod.yaml +++ b/codemods/update-react-imports/codemod.yaml @@ -1,9 +1,9 @@ schema_version: "1.0" -name: "@react-codemods/update-react-imports" +name: "@react/update-react-imports" version: "0.1.0" description: "Remove unused React default import and convert to named imports" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" @@ -11,7 +11,7 @@ workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration"] +keywords: ["React"] registry: access: "public" diff --git a/update-react-imports/package.json b/codemods/update-react-imports/package.json similarity index 79% rename from update-react-imports/package.json rename to codemods/update-react-imports/package.json index 6b56ed8..60da8b8 100644 --- a/update-react-imports/package.json +++ b/codemods/update-react-imports/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/update-react-imports", + "name": "@react/update-react-imports", "version": "0.1.0", "description": "Remove unused React default import and convert to named imports", "type": "module", @@ -9,7 +9,8 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" }, "dependencies": { "@jssg/utils": "^0.0.2" diff --git a/update-react-imports/pnpm-lock.yaml b/codemods/update-react-imports/pnpm-lock.yaml similarity index 100% rename from update-react-imports/pnpm-lock.yaml rename to codemods/update-react-imports/pnpm-lock.yaml diff --git a/codemods/update-react-imports/scripts/codemod.ts b/codemods/update-react-imports/scripts/codemod.ts new file mode 100644 index 0000000..d1e58da --- /dev/null +++ b/codemods/update-react-imports/scripts/codemod.ts @@ -0,0 +1,250 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + +type Specifier = { name: string; alias?: string; isType: boolean }; + +function isWhitespace(char: string | undefined): boolean { + return char === " " || char === "\t" || char === "\n" || char === "\r"; +} + +function sourceText(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = node.text(); + if (text.length >= 2) return text.slice(1, -1); + return null; +} + +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? sourceText(source) : null; +} + +function isReactImport(node: SgNode): boolean { + const source = importSource(node); + return source === "react" || source === "React"; +} + +function statementRange(node: SgNode, source: string): { start: number; end: number } { + const range = node.range(); + let end = range.end.index; + while (end < source.length && (source[end] === "\n" || source[end] === "\r")) end++; + return { start: range.start.index, end }; +} + +function formatSpec(spec: Specifier): string { + const name = spec.alias && spec.alias !== spec.name ? `${spec.name} as ${spec.alias}` : spec.name; + return spec.isType ? `type ${name}` : name; +} + +function uniqueSpecs(specs: Specifier[]): Specifier[] { + const seen = new Set(); + const result: Specifier[] = []; + for (const spec of specs) { + const key = `${spec.isType}:${spec.name}:${spec.alias ?? ""}`; + if (!seen.has(key)) { + seen.add(key); + result.push(spec); + } + } + return result; +} + +function splitSpecs(specs: Specifier[]): { regular: Specifier[]; type: Specifier[] } { + return { + regular: uniqueSpecs(specs.filter((s) => !s.isType)), + type: uniqueSpecs(specs.filter((s) => s.isType).map((s) => ({ ...s, isType: false }))), + }; +} + +function importText(specs: Specifier[], quote: string, typeOnly = false): string | null { + if (specs.length === 0) return null; + const body = specs.map(formatSpec).join(", "); + return typeOnly + ? `import type { ${body} } from ${quote}react${quote};\n` + : `import { ${body} } from ${quote}react${quote};\n`; +} + +function namespaceImportText(quote: string): string { + return `import * as React from ${quote}react${quote};\n`; +} + +function quoteFor(node: SgNode): string { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + const text = source?.text() ?? "\"react\""; + return text.startsWith("'") ? "'" : "\""; +} + +function getReactSpecifier(importNode: SgNode): "default" | "namespace" | null { + const clause = importNode.find({ rule: { kind: "import_clause" } }); + if (!clause) return null; + const namespace = clause.find({ rule: { kind: "namespace_import" } }); + if (namespace?.find({ rule: { kind: "identifier", regex: "^React$" } })) return "namespace"; + const firstIdentifier = clause.children().find((c) => c.kind() === "identifier"); + return firstIdentifier?.text() === "React" ? "default" : null; +} + +function namedSpecifiers(importNode: SgNode): Specifier[] { + return importNode.findAll({ rule: { kind: "import_specifier" } }) + .map((spec) => ({ + name: spec.field("name")?.text() ?? spec.text(), + alias: spec.field("alias")?.text() ?? undefined, + isType: spec.text().startsWith("type ") || importNode.text().startsWith("import type"), + })); +} + +function identifierIsInImport(node: SgNode): boolean { + return node.ancestors().some((a) => a.kind() === "import_statement"); +} + +function parentUsesIdentifierAsObject(node: SgNode): SgNode | null { + const parent = node.parent(); + if (!parent) return null; + if ( + parent.kind() === "member_expression" && + parent.field("object")?.id() === node.id() && + parent.field("property") + ) { + return parent; + } + return null; +} + +function isDeclared(rootNode: SgNode, name: string): boolean { + return rootNode.findAll({ rule: { kind: "identifier", regex: `^${name}$` } }) + .some((node) => { + if (identifierIsInImport(node)) return false; + const parent = node.parent(); + if (!parent) return true; + if (parent.kind() === "member_expression" && parent.field("object")?.id() === node.id()) { + return false; + } + if (parent.kind() === "nested_type_identifier" && parent.field("object")?.id() === node.id()) { + return false; + } + return true; + }); +} + +function removeReactImportEdits(imports: SgNode[], source: string): Edit[] { + return imports.map((imp) => { + const { start, end } = statementRange(imp, source); + return { startPos: start, endPos: end, insertedText: "" }; + }); +} + +const transform: Transform = async (root, options) => { + const rootNode = root.root(); + const source = rootNode.text(); + const metric = useMetricAtom("react-import-updates"); + const edits: Edit[] = []; + + const reactImports = rootNode.findAll({ rule: { kind: "import_statement" } }).filter(isReactImport); + const valueReactImports = reactImports.filter((imp) => + !imp.text().startsWith("import type") && getReactSpecifier(imp) !== null + + ); + if (valueReactImports.length === 0) return null; + + const primaryReactImport = valueReactImports[0]!; + const reactSpecifierKind = getReactSpecifier(primaryReactImport)!; + const quote = quoteFor(primaryReactImport); + const allNamedFromReactImports = reactImports.flatMap(namedSpecifiers); + const allExistingNamed = valueReactImports.flatMap(namedSpecifiers); + const reactIdentifiers = rootNode.findAll({ rule: { kind: "identifier", regex: "^React$" } }) + .filter((node) => !identifierIsInImport(node)); + + const memberUsages: SgNode[] = []; + let hasDirectReactUse = false; + let hasTypeNamespaceUse = false; + + for (const identifier of reactIdentifiers) { + const member = parentUsesIdentifierAsObject(identifier); + if (member) { + memberUsages.push(member); + continue; + } + const parent = identifier.parent(); + if (parent?.kind() === "nested_type_identifier" && parent.field("object")?.id() === identifier.id()) { + hasTypeNamespaceUse = true; + continue; + } + hasDirectReactUse = true; + } + + const isReactUsed = reactIdentifiers.length > 0 || hasTypeNamespaceUse; + const canDestructure = isReactUsed && !hasDirectReactUse && !hasTypeNamespaceUse; + const regularSpecs: Specifier[] = []; + const typeSpecs: Specifier[] = []; + const memberReplacementEdits: Edit[] = []; + const existingRegularNames = new Set( + allNamedFromReactImports + .filter((spec) => !spec.isType) + .flatMap((spec) => [spec.name, spec.alias].filter(Boolean) as string[]), + ); + + if (canDestructure) { + for (const member of memberUsages) { + const property = member.field("property"); + if (!property) continue; + const name = property.text(); + if (existingRegularNames.has(name) || isDeclared(rootNode, name)) { + hasDirectReactUse = true; + break; + } + regularSpecs.push({ name, isType: false }); + memberReplacementEdits.push(member.replace(name)); + } + } + + const shouldDestructure = + (reactSpecifierKind === "default" || + options.params?.destructureNamespaceImports === "true") && + canDestructure && + !hasDirectReactUse; + const splitExisting = splitSpecs(allExistingNamed); + regularSpecs.push(...splitExisting.regular); + typeSpecs.push(...splitExisting.type); + + let replacementImports = ""; + if (shouldDestructure) { + edits.push(...memberReplacementEdits); + replacementImports += importText(uniqueSpecs(typeSpecs), quote, true) ?? ""; + replacementImports += importText(uniqueSpecs(regularSpecs), quote) ?? ""; + } else { + const split = splitSpecs(allExistingNamed); + replacementImports += importText(split.type, quote, true) ?? ""; + replacementImports += importText(split.regular, quote) ?? ""; + if (isReactUsed) { + replacementImports += namespaceImportText(quote); + } + } + + const firstImport = valueReactImports[0]!; + edits.push({ + startPos: firstImport.range().start.index, + endPos: firstImport.range().start.index, + insertedText: replacementImports, + }); + edits.push(...removeReactImportEdits(valueReactImports, source)); + + metric.increment({ + action: shouldDestructure + ? "convert-member-to-named" + : isReactUsed + ? "convert-to-namespace" + : "remove-react-import", + file: metricFile(root.filename()), + previous: reactSpecifierKind, + }); + + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/expected.tsx b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/expected.tsx new file mode 100644 index 0000000..0302b77 --- /dev/null +++ b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/expected.tsx @@ -0,0 +1,5 @@ +import { createElement, useState } from "react"; +import * as React from "react"; +React.createElement('div', {}); + +
Hi
; diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/input.tsx b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/input.tsx new file mode 100644 index 0000000..b4427d5 --- /dev/null +++ b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/input.tsx @@ -0,0 +1,5 @@ +import React, { createElement, useState } from "react"; + +React.createElement('div', {}); + +
Hi
; diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/metrics.json b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/metrics.json new file mode 100644 index 0000000..21cc443 --- /dev/null +++ b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/default-and-multiple-specifiers-import-react-variable/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/expected.tsx b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/expected.tsx new file mode 100644 index 0000000..83cca58 --- /dev/null +++ b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/expected.tsx @@ -0,0 +1,3 @@ +import type { Element } from "react"; +import { createElement, useState } from "react"; +
Hi
; diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/input.tsx b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/input.tsx new file mode 100644 index 0000000..4b0f90d --- /dev/null +++ b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/input.tsx @@ -0,0 +1,3 @@ +import React, { type Element, createElement, useState } from "react"; + +
Hi
; diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/metrics.json b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/metrics.json new file mode 100644 index 0000000..bd4da64 --- /dev/null +++ b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/default-and-multiple-specifiers-import/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/update-react-imports/tests/react-as-value/expected.tsx b/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/expected.tsx similarity index 63% rename from update-react-imports/tests/react-as-value/expected.tsx rename to codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/expected.tsx index 949811d..9f5bcfc 100644 --- a/update-react-imports/tests/react-as-value/expected.tsx +++ b/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/expected.tsx @@ -1,9 +1,6 @@ import * as React from "react"; - React.createElement('div', {}); Promise.resolve(React); -function MyComponent() { - return
Hi
; -} +
Hi
diff --git a/update-react-imports/tests/original-react-not-removed/expected.tsx b/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/input.tsx similarity index 100% rename from update-react-imports/tests/original-react-not-removed/expected.tsx rename to codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/input.tsx diff --git a/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/metrics.json b/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/metrics.json new file mode 100644 index 0000000..69f37f7 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/destructure-named-imports-react-not-removed/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/test.config.json b/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/test.config.json new file mode 100644 index 0000000..319e5b9 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "destructureNamespaceImports": true + } +} diff --git a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/expected.tsx b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/expected.tsx new file mode 100644 index 0000000..ebb2ad9 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/expected.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import { createElement } from "react"; + +React.createElement('div', {}); + +
Hi
; diff --git a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/input.tsx b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/input.tsx new file mode 100644 index 0000000..ebb2ad9 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/input.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import { createElement } from "react"; + +React.createElement('div', {}); + +
Hi
; diff --git a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/metrics.json b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/metrics.json new file mode 100644 index 0000000..cc9aca4 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/destructure-named-imports-variable-used/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/test.config.json b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/test.config.json new file mode 100644 index 0000000..319e5b9 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "destructureNamespaceImports": true + } +} diff --git a/codemods/update-react-imports/tests/destructure-named-imports/expected.tsx b/codemods/update-react-imports/tests/destructure-named-imports/expected.tsx new file mode 100644 index 0000000..6eea2db --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-named-imports/expected.tsx @@ -0,0 +1,4 @@ +import * as React from "react"; +React.useState(false); + +
\ No newline at end of file diff --git a/codemods/update-react-imports/tests/destructure-named-imports/input.tsx b/codemods/update-react-imports/tests/destructure-named-imports/input.tsx new file mode 100644 index 0000000..1d6e228 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-named-imports/input.tsx @@ -0,0 +1,5 @@ +import * as React from "react"; + +React.useState(false); + +
\ No newline at end of file diff --git a/update-react-imports/tests/original-react-not-removed/metrics.json b/codemods/update-react-imports/tests/destructure-named-imports/metrics.json similarity index 57% rename from update-react-imports/tests/original-react-not-removed/metrics.json rename to codemods/update-react-imports/tests/destructure-named-imports/metrics.json index 8be2092..8ed6fac 100644 --- a/update-react-imports/tests/original-react-not-removed/metrics.json +++ b/codemods/update-react-imports/tests/destructure-named-imports/metrics.json @@ -3,8 +3,8 @@ { "cardinality": { "action": "convert-to-namespace", - "file": "tests/original-react-not-removed/input.tsx", - "reason": "react-used-as-value" + "file": "tests/destructure-named-imports/input.tsx", + "previous": "namespace" }, "count": 1 } diff --git a/codemods/update-react-imports/tests/destructure-named-imports/test.config.json b/codemods/update-react-imports/tests/destructure-named-imports/test.config.json new file mode 100644 index 0000000..319e5b9 --- /dev/null +++ b/codemods/update-react-imports/tests/destructure-named-imports/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "destructureNamespaceImports": true + } +} diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/expected.tsx b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/expected.tsx new file mode 100644 index 0000000..827ccc9 --- /dev/null +++ b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/expected.tsx @@ -0,0 +1,5 @@ +import type { Element, Component } from "react"; +import { createElement } from "react"; +createElement('div', {}); + +
Hi
; diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/input.tsx b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/input.tsx new file mode 100644 index 0000000..37bcba8 --- /dev/null +++ b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/input.tsx @@ -0,0 +1,5 @@ +import React, { type Element, type Component } from "react"; + +React.createElement('div', {}); + +
Hi
; diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/metrics.json b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/metrics.json new file mode 100644 index 0000000..7270eab --- /dev/null +++ b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-member-to-named", + "file": "tests/flow-default-and-type-specifier-import-react-variable/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/expected.tsx b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/expected.tsx new file mode 100644 index 0000000..5d45ddc --- /dev/null +++ b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/expected.tsx @@ -0,0 +1,2 @@ +import type { Element } from "react"; +
Hi
; diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/input.tsx b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/input.tsx new file mode 100644 index 0000000..b0e8765 --- /dev/null +++ b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/input.tsx @@ -0,0 +1,3 @@ +import React, { type Element } from "react"; + +
Hi
; diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/metrics.json b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/metrics.json new file mode 100644 index 0000000..5605f55 --- /dev/null +++ b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/flow-default-and-type-specifier-import/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/jsx-element/expected.tsx b/codemods/update-react-imports/tests/jsx-element/expected.tsx new file mode 100644 index 0000000..8d6e7b5 --- /dev/null +++ b/codemods/update-react-imports/tests/jsx-element/expected.tsx @@ -0,0 +1 @@ +
Hi
; diff --git a/codemods/update-react-imports/tests/jsx-element/input.tsx b/codemods/update-react-imports/tests/jsx-element/input.tsx new file mode 100644 index 0000000..0d90028 --- /dev/null +++ b/codemods/update-react-imports/tests/jsx-element/input.tsx @@ -0,0 +1,3 @@ +import * as React from "react"; + +
Hi
; diff --git a/codemods/update-react-imports/tests/jsx-element/metrics.json b/codemods/update-react-imports/tests/jsx-element/metrics.json new file mode 100644 index 0000000..23bca4b --- /dev/null +++ b/codemods/update-react-imports/tests/jsx-element/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/jsx-element/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/jsx-fragment/expected.tsx b/codemods/update-react-imports/tests/jsx-fragment/expected.tsx new file mode 100644 index 0000000..ef5e491 --- /dev/null +++ b/codemods/update-react-imports/tests/jsx-fragment/expected.tsx @@ -0,0 +1 @@ +<>; diff --git a/codemods/update-react-imports/tests/jsx-fragment/input.tsx b/codemods/update-react-imports/tests/jsx-fragment/input.tsx new file mode 100644 index 0000000..6a8c34d --- /dev/null +++ b/codemods/update-react-imports/tests/jsx-fragment/input.tsx @@ -0,0 +1,3 @@ +import * as React from "react"; + +<>; diff --git a/codemods/update-react-imports/tests/jsx-fragment/metrics.json b/codemods/update-react-imports/tests/jsx-fragment/metrics.json new file mode 100644 index 0000000..3ea49e2 --- /dev/null +++ b/codemods/update-react-imports/tests/jsx-fragment/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/jsx-fragment/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/leading-comment/expected.tsx b/codemods/update-react-imports/tests/leading-comment/expected.tsx new file mode 100644 index 0000000..f739df6 --- /dev/null +++ b/codemods/update-react-imports/tests/leading-comment/expected.tsx @@ -0,0 +1,5 @@ +/** + * Hello world. + */ + +
; diff --git a/codemods/update-react-imports/tests/leading-comment/input.tsx b/codemods/update-react-imports/tests/leading-comment/input.tsx new file mode 100644 index 0000000..0ace368 --- /dev/null +++ b/codemods/update-react-imports/tests/leading-comment/input.tsx @@ -0,0 +1,7 @@ +/** + * Hello world. + */ + +import * as React from "react"; + +
; diff --git a/codemods/update-react-imports/tests/leading-comment/metrics.json b/codemods/update-react-imports/tests/leading-comment/metrics.json new file mode 100644 index 0000000..f30d41f --- /dev/null +++ b/codemods/update-react-imports/tests/leading-comment/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/leading-comment/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-already-used-named-export/expected.tsx b/codemods/update-react-imports/tests/react-already-used-named-export/expected.tsx new file mode 100644 index 0000000..491a83a --- /dev/null +++ b/codemods/update-react-imports/tests/react-already-used-named-export/expected.tsx @@ -0,0 +1,2 @@ +import * as React from 'react'; +React.useState(false); \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-already-used-named-export/input.tsx b/codemods/update-react-imports/tests/react-already-used-named-export/input.tsx new file mode 100644 index 0000000..33f3568 --- /dev/null +++ b/codemods/update-react-imports/tests/react-already-used-named-export/input.tsx @@ -0,0 +1,3 @@ +import * as React from 'react'; + +React.useState(false); \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-already-used-named-export/metrics.json b/codemods/update-react-imports/tests/react-already-used-named-export/metrics.json new file mode 100644 index 0000000..cd15468 --- /dev/null +++ b/codemods/update-react-imports/tests/react-already-used-named-export/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/react-already-used-named-export/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/update-react-imports/tests/member-expression/expected.tsx b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/expected.tsx similarity index 54% rename from update-react-imports/tests/member-expression/expected.tsx rename to codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/expected.tsx index e62d6d3..4a3728b 100644 --- a/update-react-imports/tests/member-expression/expected.tsx +++ b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/expected.tsx @@ -1,7 +1,4 @@ import { createElement } from "react"; - createElement('div', {}); -function MyComponent() { - return
Hello
; -} +
; diff --git a/update-react-imports/tests/member-expression/input.tsx b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/input.tsx similarity index 53% rename from update-react-imports/tests/member-expression/input.tsx rename to codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/input.tsx index f1b7846..ec844c2 100644 --- a/update-react-imports/tests/member-expression/input.tsx +++ b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/input.tsx @@ -2,6 +2,4 @@ import React from "react"; React.createElement('div', {}); -function MyComponent() { - return
Hello
; -} +
; diff --git a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/metrics.json b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/metrics.json new file mode 100644 index 0000000..e7e8ea0 --- /dev/null +++ b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-member-to-named", + "file": "tests/react-basic-default-export-jsx-element-react-variable/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/expected.tsx b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/expected.tsx new file mode 100644 index 0000000..f3b72ee --- /dev/null +++ b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/expected.tsx @@ -0,0 +1 @@ +
; diff --git a/replace-act-import/tests/wildcard-import/expected.tsx b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/input.tsx similarity index 65% rename from replace-act-import/tests/wildcard-import/expected.tsx rename to codemods/update-react-imports/tests/react-basic-default-export-jsx-element/input.tsx index 3e51ec4..16c8617 100644 --- a/replace-act-import/tests/wildcard-import/expected.tsx +++ b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/input.tsx @@ -1,2 +1,3 @@ import React from "react"; -React.act(); + +
; diff --git a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/metrics.json b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/metrics.json new file mode 100644 index 0000000..7754f61 --- /dev/null +++ b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/react-basic-default-export-jsx-element/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/update-react-imports/tests/original-react-basic-default-export/expected.tsx b/codemods/update-react-imports/tests/react-basic-default-export/expected.tsx similarity index 98% rename from update-react-imports/tests/original-react-basic-default-export/expected.tsx rename to codemods/update-react-imports/tests/react-basic-default-export/expected.tsx index c4c96c0..bf4a35e 100644 --- a/update-react-imports/tests/original-react-basic-default-export/expected.tsx +++ b/codemods/update-react-imports/tests/react-basic-default-export/expected.tsx @@ -1,3 +1,2 @@ import { createElement } from "react"; - createElement('div', 'la'); diff --git a/update-react-imports/tests/original-react-basic-default-export/input.tsx b/codemods/update-react-imports/tests/react-basic-default-export/input.tsx similarity index 100% rename from update-react-imports/tests/original-react-basic-default-export/input.tsx rename to codemods/update-react-imports/tests/react-basic-default-export/input.tsx diff --git a/update-react-imports/tests/member-expression/metrics.json b/codemods/update-react-imports/tests/react-basic-default-export/metrics.json similarity index 60% rename from update-react-imports/tests/member-expression/metrics.json rename to codemods/update-react-imports/tests/react-basic-default-export/metrics.json index c6029a3..0a7a660 100644 --- a/update-react-imports/tests/member-expression/metrics.json +++ b/codemods/update-react-imports/tests/react-basic-default-export/metrics.json @@ -3,8 +3,8 @@ { "cardinality": { "action": "convert-member-to-named", - "file": "tests/member-expression/input.tsx", - "members": "createElement" + "file": "tests/react-basic-default-export/input.tsx", + "previous": "default" }, "count": 1 } diff --git a/codemods/update-react-imports/tests/react-jsx-member-expression/expected.tsx b/codemods/update-react-imports/tests/react-jsx-member-expression/expected.tsx new file mode 100644 index 0000000..dd7360f --- /dev/null +++ b/codemods/update-react-imports/tests/react-jsx-member-expression/expected.tsx @@ -0,0 +1,2 @@ +import { Fragment, useState } from 'react'; + \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-jsx-member-expression/input.tsx b/codemods/update-react-imports/tests/react-jsx-member-expression/input.tsx new file mode 100644 index 0000000..5d34788 --- /dev/null +++ b/codemods/update-react-imports/tests/react-jsx-member-expression/input.tsx @@ -0,0 +1,3 @@ +import React, { useState } from 'react'; + + \ No newline at end of file diff --git a/update-react-imports/tests/original-react-basic-default-export/metrics.json b/codemods/update-react-imports/tests/react-jsx-member-expression/metrics.json similarity index 57% rename from update-react-imports/tests/original-react-basic-default-export/metrics.json rename to codemods/update-react-imports/tests/react-jsx-member-expression/metrics.json index a2a1cba..e060601 100644 --- a/update-react-imports/tests/original-react-basic-default-export/metrics.json +++ b/codemods/update-react-imports/tests/react-jsx-member-expression/metrics.json @@ -3,8 +3,8 @@ { "cardinality": { "action": "convert-member-to-named", - "file": "tests/original-react-basic-default-export/input.tsx", - "members": "createElement" + "file": "tests/react-jsx-member-expression/input.tsx", + "previous": "default" }, "count": 1 } diff --git a/codemods/update-react-imports/tests/react-not-removed/expected.tsx b/codemods/update-react-imports/tests/react-not-removed/expected.tsx new file mode 100644 index 0000000..4b9bcea --- /dev/null +++ b/codemods/update-react-imports/tests/react-not-removed/expected.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +React.createElement('div', {}); + +Promise.resolve(React); + +
Hi
\ No newline at end of file diff --git a/update-react-imports/tests/original-react-not-removed/input.tsx b/codemods/update-react-imports/tests/react-not-removed/input.tsx similarity index 86% rename from update-react-imports/tests/original-react-not-removed/input.tsx rename to codemods/update-react-imports/tests/react-not-removed/input.tsx index 1b5b27c..73b0dae 100644 --- a/update-react-imports/tests/original-react-not-removed/input.tsx +++ b/codemods/update-react-imports/tests/react-not-removed/input.tsx @@ -4,4 +4,4 @@ React.createElement('div', {}); Promise.resolve(React); -
Hi
+
Hi
\ No newline at end of file diff --git a/update-react-imports/tests/react-as-value/metrics.json b/codemods/update-react-imports/tests/react-not-removed/metrics.json similarity index 60% rename from update-react-imports/tests/react-as-value/metrics.json rename to codemods/update-react-imports/tests/react-not-removed/metrics.json index d0cd9f4..8ab16c4 100644 --- a/update-react-imports/tests/react-as-value/metrics.json +++ b/codemods/update-react-imports/tests/react-not-removed/metrics.json @@ -3,8 +3,8 @@ { "cardinality": { "action": "convert-to-namespace", - "file": "tests/react-as-value/input.tsx", - "reason": "react-used-as-value" + "file": "tests/react-not-removed/input.tsx", + "previous": "default" }, "count": 1 } diff --git a/codemods/update-react-imports/tests/react-type-default-export/expected.tsx b/codemods/update-react-imports/tests/react-type-default-export/expected.tsx new file mode 100644 index 0000000..36e2cbd --- /dev/null +++ b/codemods/update-react-imports/tests/react-type-default-export/expected.tsx @@ -0,0 +1,2 @@ +import type React from "react"; +
Hi
; diff --git a/codemods/update-react-imports/tests/react-type-default-export/input.tsx b/codemods/update-react-imports/tests/react-type-default-export/input.tsx new file mode 100644 index 0000000..65515a7 --- /dev/null +++ b/codemods/update-react-imports/tests/react-type-default-export/input.tsx @@ -0,0 +1,4 @@ +import type React from "react"; +import * as React from "react"; + +
Hi
; diff --git a/codemods/update-react-imports/tests/react-type-default-export/metrics.json b/codemods/update-react-imports/tests/react-type-default-export/metrics.json new file mode 100644 index 0000000..53bdfa4 --- /dev/null +++ b/codemods/update-react-imports/tests/react-type-default-export/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/react-type-default-export/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-type-not-removed/expected.tsx b/codemods/update-react-imports/tests/react-type-not-removed/expected.tsx new file mode 100644 index 0000000..0dcfc2b --- /dev/null +++ b/codemods/update-react-imports/tests/react-type-not-removed/expected.tsx @@ -0,0 +1,2 @@ +import type React, { Node } from "react"; +
Hi
; diff --git a/codemods/update-react-imports/tests/react-type-not-removed/input.tsx b/codemods/update-react-imports/tests/react-type-not-removed/input.tsx new file mode 100644 index 0000000..3c71fbf --- /dev/null +++ b/codemods/update-react-imports/tests/react-type-not-removed/input.tsx @@ -0,0 +1,4 @@ +import type React, { Node } from "react"; +import * as React from "react"; + +
Hi
; diff --git a/codemods/update-react-imports/tests/react-type-not-removed/metrics.json b/codemods/update-react-imports/tests/react-type-not-removed/metrics.json new file mode 100644 index 0000000..0218dd9 --- /dev/null +++ b/codemods/update-react-imports/tests/react-type-not-removed/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/react-type-not-removed/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/expected.tsx new file mode 100644 index 0000000..0302b77 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/expected.tsx @@ -0,0 +1,5 @@ +import { createElement, useState } from "react"; +import * as React from "react"; +React.createElement('div', {}); + +
Hi
; diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/input.tsx new file mode 100644 index 0000000..b4427d5 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/input.tsx @@ -0,0 +1,5 @@ +import React, { createElement, useState } from "react"; + +React.createElement('div', {}); + +
Hi
; diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/metrics.json new file mode 100644 index 0000000..fc22dae --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/expected.tsx new file mode 100644 index 0000000..d617eef --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/expected.tsx @@ -0,0 +1,2 @@ +import { createElement, useState } from "react"; +
Hi
; diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/input.tsx new file mode 100644 index 0000000..e2b936a --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/input.tsx @@ -0,0 +1,3 @@ +import React, { createElement, useState } from "react"; + +
Hi
; diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/metrics.json new file mode 100644 index 0000000..c1b188c --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/typescript-default-and-multiple-specifiers-import-tsx/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-jsx-element-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-jsx-element-tsx/expected.tsx new file mode 100644 index 0000000..8d6e7b5 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-jsx-element-tsx/expected.tsx @@ -0,0 +1 @@ +
Hi
; diff --git a/codemods/update-react-imports/tests/typescript-jsx-element-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-jsx-element-tsx/input.tsx new file mode 100644 index 0000000..0d90028 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-jsx-element-tsx/input.tsx @@ -0,0 +1,3 @@ +import * as React from "react"; + +
Hi
; diff --git a/codemods/update-react-imports/tests/typescript-jsx-element-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-jsx-element-tsx/metrics.json new file mode 100644 index 0000000..5c3bb6b --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-jsx-element-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/typescript-jsx-element-tsx/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/expected.tsx new file mode 100644 index 0000000..ef5e491 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/expected.tsx @@ -0,0 +1 @@ +<>; diff --git a/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/input.tsx new file mode 100644 index 0000000..6a8c34d --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/input.tsx @@ -0,0 +1,3 @@ +import * as React from "react"; + +<>; diff --git a/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/metrics.json new file mode 100644 index 0000000..01635eb --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/typescript-jsx-fragment-tsx/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-leading-comment-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-leading-comment-tsx/expected.tsx new file mode 100644 index 0000000..f739df6 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-leading-comment-tsx/expected.tsx @@ -0,0 +1,5 @@ +/** + * Hello world. + */ + +
; diff --git a/codemods/update-react-imports/tests/typescript-leading-comment-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-leading-comment-tsx/input.tsx new file mode 100644 index 0000000..0ace368 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-leading-comment-tsx/input.tsx @@ -0,0 +1,7 @@ +/** + * Hello world. + */ + +import * as React from "react"; + +
; diff --git a/codemods/update-react-imports/tests/typescript-leading-comment-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-leading-comment-tsx/metrics.json new file mode 100644 index 0000000..d8e4624 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-leading-comment-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/typescript-leading-comment-tsx/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/expected.tsx new file mode 100644 index 0000000..f9f64e1 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/expected.tsx @@ -0,0 +1,4 @@ +import * as React from 'react'; +const App: React.FunctionComponent<{ message: string }> = ({ message }) => ( +
{message}
+); diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/input.tsx new file mode 100644 index 0000000..2f8f600 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/input.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +const App: React.FunctionComponent<{ message: string }> = ({ message }) => ( +
{message}
+); diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/metrics.json new file mode 100644 index 0000000..6918c08 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/typescript-preserve-types-default-tsx/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/expected.tsx new file mode 100644 index 0000000..f9f64e1 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/expected.tsx @@ -0,0 +1,4 @@ +import * as React from 'react'; +const App: React.FunctionComponent<{ message: string }> = ({ message }) => ( +
{message}
+); diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/input.tsx new file mode 100644 index 0000000..f27298b --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/input.tsx @@ -0,0 +1,5 @@ +import * as React from 'react'; + +const App: React.FunctionComponent<{ message: string }> = ({ message }) => ( +
{message}
+); diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/metrics.json new file mode 100644 index 0000000..6f82fd0 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/typescript-preserve-types-namespace-tsx/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/expected.tsx new file mode 100644 index 0000000..491a83a --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/expected.tsx @@ -0,0 +1,2 @@ +import * as React from 'react'; +React.useState(false); \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/input.tsx new file mode 100644 index 0000000..33f3568 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/input.tsx @@ -0,0 +1,3 @@ +import * as React from 'react'; + +React.useState(false); \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/metrics.json new file mode 100644 index 0000000..f5ea9f4 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/typescript-react-already-used-named-export-tsx/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/expected.tsx new file mode 100644 index 0000000..4a3728b --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/expected.tsx @@ -0,0 +1,4 @@ +import { createElement } from "react"; +createElement('div', {}); + +
; diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/input.tsx new file mode 100644 index 0000000..ec844c2 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/input.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +React.createElement('div', {}); + +
; diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/metrics.json new file mode 100644 index 0000000..bed66ce --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-member-to-named", + "file": "tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/expected.tsx new file mode 100644 index 0000000..f3b72ee --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/expected.tsx @@ -0,0 +1 @@ +
; diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/input.tsx new file mode 100644 index 0000000..16c8617 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/input.tsx @@ -0,0 +1,3 @@ +import React from "react"; + +
; diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/metrics.json new file mode 100644 index 0000000..5776d05 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "remove-react-import", + "file": "tests/typescript-react-basic-default-export-jsx-element-tsx/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/expected.tsx new file mode 100644 index 0000000..bf4a35e --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/expected.tsx @@ -0,0 +1,2 @@ +import { createElement } from "react"; +createElement('div', 'la'); diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/input.tsx new file mode 100644 index 0000000..37f5d41 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/input.tsx @@ -0,0 +1,3 @@ +import React from "react"; + +React.createElement('div', 'la'); diff --git a/update-react-imports/tests/react-fragment-member/metrics.json b/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/metrics.json similarity index 50% rename from update-react-imports/tests/react-fragment-member/metrics.json rename to codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/metrics.json index d840a61..99b4fbe 100644 --- a/update-react-imports/tests/react-fragment-member/metrics.json +++ b/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/metrics.json @@ -3,10 +3,10 @@ { "cardinality": { "action": "convert-member-to-named", - "file": "tests/react-fragment-member/input.tsx", - "members": "Fragment" + "file": "tests/typescript-react-basic-default-export-tsx/input.tsx", + "previous": "default" }, - "count": 2 + "count": 1 } ] } \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/expected.tsx new file mode 100644 index 0000000..dd7360f --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/expected.tsx @@ -0,0 +1,2 @@ +import { Fragment, useState } from 'react'; + \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/input.tsx new file mode 100644 index 0000000..5d34788 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/input.tsx @@ -0,0 +1,3 @@ +import React, { useState } from 'react'; + + \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/metrics.json new file mode 100644 index 0000000..4224bed --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-member-to-named", + "file": "tests/typescript-react-jsx-member-expression-tsx/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/expected.tsx new file mode 100644 index 0000000..4b9bcea --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/expected.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +React.createElement('div', {}); + +Promise.resolve(React); + +
Hi
\ No newline at end of file diff --git a/update-react-imports/tests/react-as-value/input.tsx b/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/input.tsx similarity index 62% rename from update-react-imports/tests/react-as-value/input.tsx rename to codemods/update-react-imports/tests/typescript-react-not-removed-tsx/input.tsx index 8a75314..73b0dae 100644 --- a/update-react-imports/tests/react-as-value/input.tsx +++ b/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/input.tsx @@ -4,6 +4,4 @@ React.createElement('div', {}); Promise.resolve(React); -function MyComponent() { - return
Hi
; -} +
Hi
\ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/metrics.json new file mode 100644 index 0000000..6acf7b7 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/typescript-react-not-removed-tsx/input.tsx", + "previous": "default" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/expected.tsx new file mode 100644 index 0000000..48abf6f --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/expected.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +React.createElement('div', {}); + +createElement('someFunction'); + +
Hi
; diff --git a/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/input.tsx b/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/input.tsx new file mode 100644 index 0000000..c2d7852 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/input.tsx @@ -0,0 +1,7 @@ +import * as React from "react"; + +React.createElement('div', {}); + +createElement('someFunction'); + +
Hi
; diff --git a/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/metrics.json new file mode 100644 index 0000000..03439b9 --- /dev/null +++ b/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/typescript-variable-already-used-tsx/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/variable-already-used/expected.tsx b/codemods/update-react-imports/tests/variable-already-used/expected.tsx new file mode 100644 index 0000000..48abf6f --- /dev/null +++ b/codemods/update-react-imports/tests/variable-already-used/expected.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +React.createElement('div', {}); + +createElement('someFunction'); + +
Hi
; diff --git a/codemods/update-react-imports/tests/variable-already-used/input.tsx b/codemods/update-react-imports/tests/variable-already-used/input.tsx new file mode 100644 index 0000000..c2d7852 --- /dev/null +++ b/codemods/update-react-imports/tests/variable-already-used/input.tsx @@ -0,0 +1,7 @@ +import * as React from "react"; + +React.createElement('div', {}); + +createElement('someFunction'); + +
Hi
; diff --git a/codemods/update-react-imports/tests/variable-already-used/metrics.json b/codemods/update-react-imports/tests/variable-already-used/metrics.json new file mode 100644 index 0000000..cb2660e --- /dev/null +++ b/codemods/update-react-imports/tests/variable-already-used/metrics.json @@ -0,0 +1,12 @@ +{ + "react-import-updates": [ + { + "cardinality": { + "action": "convert-to-namespace", + "file": "tests/variable-already-used/input.tsx", + "previous": "namespace" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/update-react-imports/tsconfig.json b/codemods/update-react-imports/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/update-react-imports/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/use-context-hook/workflow.yaml b/codemods/update-react-imports/workflow.yaml similarity index 60% rename from use-context-hook/workflow.yaml rename to codemods/update-react-imports/workflow.yaml index 8773841..f7d8d8c 100644 --- a/use-context-hook/workflow.yaml +++ b/codemods/update-react-imports/workflow.yaml @@ -2,6 +2,14 @@ version: "1" +params: + schema: + destructureNamespaceImports: + name: "Destructure Namespace Imports" + description: "Allow namespace React imports to be rewritten to named imports when it is safe." + type: boolean + default: false + nodes: - id: apply-transforms name: Apply AST Transformations diff --git a/use-context-hook/.gitignore b/codemods/use-context-hook/.gitignore similarity index 100% rename from use-context-hook/.gitignore rename to codemods/use-context-hook/.gitignore diff --git a/codemods/use-context-hook/README.md b/codemods/use-context-hook/README.md new file mode 100644 index 0000000..bf2cc57 --- /dev/null +++ b/codemods/use-context-hook/README.md @@ -0,0 +1,16 @@ +# @react/use-context-hook + +Transform `React.useContext()` and `useContext()` calls into `use()`. + +## Usage + +```bash +codemod run @react/use-context-hook +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/use-context-hook/codemod.yaml b/codemods/use-context-hook/codemod.yaml similarity index 70% rename from use-context-hook/codemod.yaml rename to codemods/use-context-hook/codemod.yaml index 930358f..f23fedd 100644 --- a/use-context-hook/codemod.yaml +++ b/codemods/use-context-hook/codemod.yaml @@ -1,9 +1,9 @@ schema_version: "1.0" -name: "@react-codemods/use-context-hook" +name: "@react/use-context-hook" version: "0.1.0" description: "Transform React.useContext() and useContext() to use() hook" -author: "React Codemods" +author: "Codemod " license: "MIT" workflow: "workflow.yaml" @@ -11,7 +11,7 @@ workflow: "workflow.yaml" targets: languages: ["tsx"] -keywords: ["transformation", "migration"] +keywords: ["React"] registry: access: "public" diff --git a/use-context-hook/package.json b/codemods/use-context-hook/package.json similarity index 80% rename from use-context-hook/package.json rename to codemods/use-context-hook/package.json index 405844f..28b51bf 100644 --- a/use-context-hook/package.json +++ b/codemods/use-context-hook/package.json @@ -1,5 +1,5 @@ { - "name": "@react-codemods/use-context-hook", + "name": "@react/use-context-hook", "version": "0.1.0", "description": "Transform React.useContext() and useContext() to use() hook", "type": "module", @@ -9,7 +9,8 @@ }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest" + "typescript": "latest", + "@types/node": "latest" }, "dependencies": { "@jssg/utils": "^0.0.2" diff --git a/use-context-hook/pnpm-lock.yaml b/codemods/use-context-hook/pnpm-lock.yaml similarity index 100% rename from use-context-hook/pnpm-lock.yaml rename to codemods/use-context-hook/pnpm-lock.yaml diff --git a/use-context-hook/scripts/codemod.ts b/codemods/use-context-hook/scripts/codemod.ts similarity index 92% rename from use-context-hook/scripts/codemod.ts rename to codemods/use-context-hook/scripts/codemod.ts index f1cd222..04c6f20 100644 --- a/use-context-hook/scripts/codemod.ts +++ b/codemods/use-context-hook/scripts/codemod.ts @@ -3,6 +3,12 @@ import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; import { getImport, addImport, removeImport } from "@jssg/utils/javascript/imports"; +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + + const transform: Transform = async (root) => { const rootNode = root.root(); const edits: Edit[] = []; @@ -55,7 +61,7 @@ const transform: Transform = async (root) => { edits.push(propertyNode.replace("use")); transformMetric.increment({ pattern: "React.useContext", - file: root.filename() + file: metricFile(root.filename()) }); } } @@ -90,14 +96,14 @@ const transform: Transform = async (root) => { edits.push(callee.replace("use")); transformMetric.increment({ pattern: "useContext", - file: root.filename() + file: metricFile(root.filename()) }); } } } else { transformMetric.increment({ pattern: "useContext (aliased)", - file: root.filename() + file: metricFile(root.filename()) }, useContextCalls.length); } diff --git a/codemods/use-context-hook/tests/any-use-context/expected.tsx b/codemods/use-context-hook/tests/any-use-context/expected.tsx new file mode 100644 index 0000000..75bfeb5 --- /dev/null +++ b/codemods/use-context-hook/tests/any-use-context/expected.tsx @@ -0,0 +1 @@ +const theme = trpc.useContext(); \ No newline at end of file diff --git a/codemods/use-context-hook/tests/any-use-context/input.tsx b/codemods/use-context-hook/tests/any-use-context/input.tsx new file mode 100644 index 0000000..75bfeb5 --- /dev/null +++ b/codemods/use-context-hook/tests/any-use-context/input.tsx @@ -0,0 +1 @@ +const theme = trpc.useContext(); \ No newline at end of file diff --git a/use-context-hook/tests/original-mixed-import/expected.tsx b/codemods/use-context-hook/tests/mixed-import/expected.tsx similarity index 70% rename from use-context-hook/tests/original-mixed-import/expected.tsx rename to codemods/use-context-hook/tests/mixed-import/expected.tsx index 6eeb2ba..7901080 100644 --- a/use-context-hook/tests/original-mixed-import/expected.tsx +++ b/codemods/use-context-hook/tests/mixed-import/expected.tsx @@ -1,4 +1,4 @@ import React, { use } from "react"; import ThemeContext from "./ThemeContext"; -const theme = use(ThemeContext); +const theme = use(ThemeContext); \ No newline at end of file diff --git a/use-context-hook/tests/original-mixed-import/input.tsx b/codemods/use-context-hook/tests/mixed-import/input.tsx similarity index 68% rename from use-context-hook/tests/original-mixed-import/input.tsx rename to codemods/use-context-hook/tests/mixed-import/input.tsx index 0049e9b..fb91bfd 100644 --- a/use-context-hook/tests/original-mixed-import/input.tsx +++ b/codemods/use-context-hook/tests/mixed-import/input.tsx @@ -1,4 +1,4 @@ import React, { useContext } from "react"; import ThemeContext from "./ThemeContext"; -const theme = useContext(ThemeContext); +const theme = useContext(ThemeContext); \ No newline at end of file diff --git a/use-context-hook/tests/mixed-import/metrics.json b/codemods/use-context-hook/tests/mixed-import/metrics.json similarity index 100% rename from use-context-hook/tests/mixed-import/metrics.json rename to codemods/use-context-hook/tests/mixed-import/metrics.json diff --git a/codemods/use-context-hook/tests/typescript-any-use-context/expected.tsx b/codemods/use-context-hook/tests/typescript-any-use-context/expected.tsx new file mode 100644 index 0000000..88397ec --- /dev/null +++ b/codemods/use-context-hook/tests/typescript-any-use-context/expected.tsx @@ -0,0 +1,8 @@ +function Component({ + appUrl, +}: { + appUrl: string; +}) { + const theme = trpc.useContext(); + return
; +}; \ No newline at end of file diff --git a/codemods/use-context-hook/tests/typescript-any-use-context/input.tsx b/codemods/use-context-hook/tests/typescript-any-use-context/input.tsx new file mode 100644 index 0000000..88397ec --- /dev/null +++ b/codemods/use-context-hook/tests/typescript-any-use-context/input.tsx @@ -0,0 +1,8 @@ +function Component({ + appUrl, +}: { + appUrl: string; +}) { + const theme = trpc.useContext(); + return
; +}; \ No newline at end of file diff --git a/use-context-hook/tests/react-member-expression/expected.tsx b/codemods/use-context-hook/tests/typescript-use-context-2/expected.tsx similarity index 51% rename from use-context-hook/tests/react-member-expression/expected.tsx rename to codemods/use-context-hook/tests/typescript-use-context-2/expected.tsx index 9982c7c..a873ec9 100644 --- a/use-context-hook/tests/react-member-expression/expected.tsx +++ b/codemods/use-context-hook/tests/typescript-use-context-2/expected.tsx @@ -1,9 +1,11 @@ import React from "react"; import ThemeContext from "./ThemeContext"; -function MyComponent() { +function Component({ + appUrl, +}: { + appUrl: string; +}) { const theme = React.use(ThemeContext); - const user = React.use(UserContext); - - return
{theme.color}
; -} + return
; +}; \ No newline at end of file diff --git a/use-context-hook/tests/react-member-expression/input.tsx b/codemods/use-context-hook/tests/typescript-use-context-2/input.tsx similarity index 51% rename from use-context-hook/tests/react-member-expression/input.tsx rename to codemods/use-context-hook/tests/typescript-use-context-2/input.tsx index 8c1a117..062dab5 100644 --- a/use-context-hook/tests/react-member-expression/input.tsx +++ b/codemods/use-context-hook/tests/typescript-use-context-2/input.tsx @@ -1,9 +1,11 @@ import React from "react"; import ThemeContext from "./ThemeContext"; -function MyComponent() { +function Component({ + appUrl, +}: { + appUrl: string; +}) { const theme = React.useContext(ThemeContext); - const user = React.useContext(UserContext); - - return
{theme.color}
; -} + return
; +}; \ No newline at end of file diff --git a/use-context-hook/tests/react-member-expression/metrics.json b/codemods/use-context-hook/tests/typescript-use-context-2/metrics.json similarity index 61% rename from use-context-hook/tests/react-member-expression/metrics.json rename to codemods/use-context-hook/tests/typescript-use-context-2/metrics.json index 7489301..c1aaf10 100644 --- a/use-context-hook/tests/react-member-expression/metrics.json +++ b/codemods/use-context-hook/tests/typescript-use-context-2/metrics.json @@ -2,10 +2,10 @@ "use-context-transformations": [ { "cardinality": { - "file": "tests/react-member-expression/input.tsx", + "file": "tests/typescript-use-context-2/input.tsx", "pattern": "React.useContext" }, - "count": 2 + "count": 1 } ] } \ No newline at end of file diff --git a/use-context-hook/tests/original-ts-use-context/expected.tsx b/codemods/use-context-hook/tests/typescript-use-context/expected.tsx similarity index 98% rename from use-context-hook/tests/original-ts-use-context/expected.tsx rename to codemods/use-context-hook/tests/typescript-use-context/expected.tsx index 6ce9e77..d44d7ae 100644 --- a/use-context-hook/tests/original-ts-use-context/expected.tsx +++ b/codemods/use-context-hook/tests/typescript-use-context/expected.tsx @@ -8,4 +8,4 @@ function Component({ }) { const theme = use(ThemeContext); return
; -}; +}; \ No newline at end of file diff --git a/use-context-hook/tests/original-ts-use-context/input.tsx b/codemods/use-context-hook/tests/typescript-use-context/input.tsx similarity index 98% rename from use-context-hook/tests/original-ts-use-context/input.tsx rename to codemods/use-context-hook/tests/typescript-use-context/input.tsx index 4e28896..7464058 100644 --- a/use-context-hook/tests/original-ts-use-context/input.tsx +++ b/codemods/use-context-hook/tests/typescript-use-context/input.tsx @@ -8,4 +8,4 @@ function Component({ }) { const theme = useContext(ThemeContext); return
; -}; +}; \ No newline at end of file diff --git a/use-context-hook/tests/original-mixed-import/metrics.json b/codemods/use-context-hook/tests/typescript-use-context/metrics.json similarity index 69% rename from use-context-hook/tests/original-mixed-import/metrics.json rename to codemods/use-context-hook/tests/typescript-use-context/metrics.json index 22eacfd..98a6998 100644 --- a/use-context-hook/tests/original-mixed-import/metrics.json +++ b/codemods/use-context-hook/tests/typescript-use-context/metrics.json @@ -2,7 +2,7 @@ "use-context-transformations": [ { "cardinality": { - "file": "tests/original-mixed-import/input.tsx", + "file": "tests/typescript-use-context/input.tsx", "pattern": "useContext" }, "count": 1 diff --git a/use-context-hook/tests/original-use-context-2/expected.tsx b/codemods/use-context-hook/tests/use-context-2/expected.tsx similarity index 64% rename from use-context-hook/tests/original-use-context-2/expected.tsx rename to codemods/use-context-hook/tests/use-context-2/expected.tsx index a126e89..f7f504f 100644 --- a/use-context-hook/tests/original-use-context-2/expected.tsx +++ b/codemods/use-context-hook/tests/use-context-2/expected.tsx @@ -1,4 +1,4 @@ import React from "react"; import ThemeContext from "./ThemeContext"; -const theme = React.use(ThemeContext); +const theme = React.use(ThemeContext); \ No newline at end of file diff --git a/use-context-hook/tests/original-use-context-2/input.tsx b/codemods/use-context-hook/tests/use-context-2/input.tsx similarity index 60% rename from use-context-hook/tests/original-use-context-2/input.tsx rename to codemods/use-context-hook/tests/use-context-2/input.tsx index 5aa2564..67718d9 100644 --- a/use-context-hook/tests/original-use-context-2/input.tsx +++ b/codemods/use-context-hook/tests/use-context-2/input.tsx @@ -1,4 +1,4 @@ import React from "react"; import ThemeContext from "./ThemeContext"; -const theme = React.useContext(ThemeContext); +const theme = React.useContext(ThemeContext); \ No newline at end of file diff --git a/use-context-hook/tests/original-use-context-2/metrics.json b/codemods/use-context-hook/tests/use-context-2/metrics.json similarity index 70% rename from use-context-hook/tests/original-use-context-2/metrics.json rename to codemods/use-context-hook/tests/use-context-2/metrics.json index eeb8696..5e0a448 100644 --- a/use-context-hook/tests/original-use-context-2/metrics.json +++ b/codemods/use-context-hook/tests/use-context-2/metrics.json @@ -2,7 +2,7 @@ "use-context-transformations": [ { "cardinality": { - "file": "tests/original-use-context-2/input.tsx", + "file": "tests/use-context-2/input.tsx", "pattern": "React.useContext" }, "count": 1 diff --git a/use-context-hook/tests/original-use-context/expected.tsx b/codemods/use-context-hook/tests/use-context/expected.tsx similarity index 68% rename from use-context-hook/tests/original-use-context/expected.tsx rename to codemods/use-context-hook/tests/use-context/expected.tsx index bb7281c..e251b57 100644 --- a/use-context-hook/tests/original-use-context/expected.tsx +++ b/codemods/use-context-hook/tests/use-context/expected.tsx @@ -1,4 +1,4 @@ import { use } from "react"; import ThemeContext from "./ThemeContext"; -const theme = use(ThemeContext); +const theme = use(ThemeContext); \ No newline at end of file diff --git a/use-context-hook/tests/original-use-context/input.tsx b/codemods/use-context-hook/tests/use-context/input.tsx similarity index 66% rename from use-context-hook/tests/original-use-context/input.tsx rename to codemods/use-context-hook/tests/use-context/input.tsx index c206438..979a8e4 100644 --- a/use-context-hook/tests/original-use-context/input.tsx +++ b/codemods/use-context-hook/tests/use-context/input.tsx @@ -1,4 +1,4 @@ import { useContext } from "react"; import ThemeContext from "./ThemeContext"; -const theme = useContext(ThemeContext); +const theme = useContext(ThemeContext); \ No newline at end of file diff --git a/use-context-hook/tests/original-use-context/metrics.json b/codemods/use-context-hook/tests/use-context/metrics.json similarity index 70% rename from use-context-hook/tests/original-use-context/metrics.json rename to codemods/use-context-hook/tests/use-context/metrics.json index f2a8df1..06a884c 100644 --- a/use-context-hook/tests/original-use-context/metrics.json +++ b/codemods/use-context-hook/tests/use-context/metrics.json @@ -2,7 +2,7 @@ "use-context-transformations": [ { "cardinality": { - "file": "tests/original-use-context/input.tsx", + "file": "tests/use-context/input.tsx", "pattern": "useContext" }, "count": 1 diff --git a/codemods/use-context-hook/tsconfig.json b/codemods/use-context-hook/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/use-context-hook/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/replace-reactdom-render/workflow.yaml b/codemods/use-context-hook/workflow.yaml similarity index 100% rename from replace-reactdom-render/workflow.yaml rename to codemods/use-context-hook/workflow.yaml diff --git a/error-boundaries/tests/arrow-assignment/expected.tsx b/error-boundaries/tests/arrow-assignment/expected.tsx deleted file mode 100644 index 32a7b9f..0000000 --- a/error-boundaries/tests/arrow-assignment/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -class Boundary extends React.Component { - componentDidCatch = (error: Error, errorInfo: React.ErrorInfo) => { - this.setState({ error }); - }; - - render() { - return this.props.children; - } -} diff --git a/error-boundaries/tests/arrow-assignment/input.tsx b/error-boundaries/tests/arrow-assignment/input.tsx deleted file mode 100644 index abfbf69..0000000 --- a/error-boundaries/tests/arrow-assignment/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -class Boundary extends React.Component { - unstable_handleError = (error: Error, errorInfo: React.ErrorInfo) => { - this.setState({ error }); - }; - - render() { - return this.props.children; - } -} diff --git a/error-boundaries/tests/arrow-assignment/metrics.json b/error-boundaries/tests/arrow-assignment/metrics.json deleted file mode 100644 index 6104550..0000000 --- a/error-boundaries/tests/arrow-assignment/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error-boundary-renames": [ - { - "cardinality": { - "file": "tests/arrow-assignment/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/error-boundaries/tests/export-default-class/expected.tsx b/error-boundaries/tests/export-default-class/expected.tsx deleted file mode 100644 index 52ae77c..0000000 --- a/error-boundaries/tests/export-default-class/expected.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; - -export default class AppErrorBoundary extends React.Component { - state = { hasError: false }; - - componentDidCatch(error, errorInfo) { - this.setState({ hasError: true }); - } - - render() { - return this.props.children; - } -} diff --git a/error-boundaries/tests/export-default-class/input.tsx b/error-boundaries/tests/export-default-class/input.tsx deleted file mode 100644 index f195793..0000000 --- a/error-boundaries/tests/export-default-class/input.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; - -export default class AppErrorBoundary extends React.Component { - state = { hasError: false }; - - unstable_handleError(error, errorInfo) { - this.setState({ hasError: true }); - } - - render() { - return this.props.children; - } -} diff --git a/error-boundaries/tests/export-default-class/metrics.json b/error-boundaries/tests/export-default-class/metrics.json deleted file mode 100644 index 571d898..0000000 --- a/error-boundaries/tests/export-default-class/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error-boundary-renames": [ - { - "cardinality": { - "file": "tests/export-default-class/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/error-boundaries/tests/mixed-methods/expected.tsx b/error-boundaries/tests/mixed-methods/expected.tsx deleted file mode 100644 index 080e170..0000000 --- a/error-boundaries/tests/mixed-methods/expected.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; - -class ErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = { hasError: false }; - } - - componentDidCatch(error, errorInfo) { - this.setState({ hasError: true }); - console.log(error, errorInfo); - } - - componentDidMount() { - console.log("mounted"); - } - - render() { - if (this.state.hasError) { - return

Something went wrong.

; - } - return this.props.children; - } -} - -export default ErrorBoundary; diff --git a/error-boundaries/tests/mixed-methods/input.tsx b/error-boundaries/tests/mixed-methods/input.tsx deleted file mode 100644 index 2b8215b..0000000 --- a/error-boundaries/tests/mixed-methods/input.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from "react"; - -class ErrorBoundary extends React.Component { - constructor(props) { - super(props); - this.state = { hasError: false }; - } - - unstable_handleError(error, errorInfo) { - this.setState({ hasError: true }); - console.log(error, errorInfo); - } - - componentDidMount() { - console.log("mounted"); - } - - render() { - if (this.state.hasError) { - return

Something went wrong.

; - } - return this.props.children; - } -} - -export default ErrorBoundary; diff --git a/error-boundaries/tests/mixed-methods/metrics.json b/error-boundaries/tests/mixed-methods/metrics.json deleted file mode 100644 index c73ce75..0000000 --- a/error-boundaries/tests/mixed-methods/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error-boundary-renames": [ - { - "cardinality": { - "file": "tests/mixed-methods/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/error-boundaries/tests/no-unstable-handleError/expected.tsx b/error-boundaries/tests/no-unstable-handleError/expected.tsx deleted file mode 100644 index 3e9309c..0000000 --- a/error-boundaries/tests/no-unstable-handleError/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -export class MyComponent extends React.Component { - componentDidCatch(error, errorInfo) { - console.log(error, errorInfo); - } - - render() { - return
Hello
; - } -} diff --git a/error-boundaries/tests/no-unstable-handleError/input.tsx b/error-boundaries/tests/no-unstable-handleError/input.tsx deleted file mode 100644 index 3e9309c..0000000 --- a/error-boundaries/tests/no-unstable-handleError/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -export class MyComponent extends React.Component { - componentDidCatch(error, errorInfo) { - console.log(error, errorInfo); - } - - render() { - return
Hello
; - } -} diff --git a/error-boundaries/tests/object-literal-method/expected.tsx b/error-boundaries/tests/object-literal-method/expected.tsx deleted file mode 100644 index 18e6f00..0000000 --- a/error-boundaries/tests/object-literal-method/expected.tsx +++ /dev/null @@ -1,8 +0,0 @@ -const config = { - unstable_handleError(err: Error) { - report(err); - }, - onRetry() {}, -}; - -export const ErrorHandler = () => null; diff --git a/error-boundaries/tests/object-literal-method/input.tsx b/error-boundaries/tests/object-literal-method/input.tsx deleted file mode 100644 index 18e6f00..0000000 --- a/error-boundaries/tests/object-literal-method/input.tsx +++ /dev/null @@ -1,8 +0,0 @@ -const config = { - unstable_handleError(err: Error) { - report(err); - }, - onRetry() {}, -}; - -export const ErrorHandler = () => null; diff --git a/error-boundaries/tests/real-world-dashboard-error-boundary/expected.tsx b/error-boundaries/tests/real-world-dashboard-error-boundary/expected.tsx deleted file mode 100644 index 9f63c62..0000000 --- a/error-boundaries/tests/real-world-dashboard-error-boundary/expected.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -export class DashboardErrorBoundary extends React.Component { - state = { hasError: false }; - - componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - this.setState({ hasError: true }); - logErrorToService(error, errorInfo.componentStack); - } - - render() { - if (this.state.hasError) { - return
Something went wrong.
; - } - return this.props.children; - } -} - -function logErrorToService(_err: Error, _stack?: string) {} diff --git a/error-boundaries/tests/real-world-dashboard-error-boundary/input.tsx b/error-boundaries/tests/real-world-dashboard-error-boundary/input.tsx deleted file mode 100644 index d514b03..0000000 --- a/error-boundaries/tests/real-world-dashboard-error-boundary/input.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from "react"; - -export class DashboardErrorBoundary extends React.Component { - state = { hasError: false }; - - unstable_handleError(error: Error, errorInfo: React.ErrorInfo) { - this.setState({ hasError: true }); - logErrorToService(error, errorInfo.componentStack); - } - - render() { - if (this.state.hasError) { - return
Something went wrong.
; - } - return this.props.children; - } -} - -function logErrorToService(_err: Error, _stack?: string) {} diff --git a/error-boundaries/tests/real-world-dashboard-error-boundary/metrics.json b/error-boundaries/tests/real-world-dashboard-error-boundary/metrics.json deleted file mode 100644 index f386676..0000000 --- a/error-boundaries/tests/real-world-dashboard-error-boundary/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error-boundary-renames": [ - { - "cardinality": { - "file": "tests/real-world-dashboard-error-boundary/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/error-boundaries/tests/similar-name-no-op/expected.tsx b/error-boundaries/tests/similar-name-no-op/expected.tsx deleted file mode 100644 index 336384f..0000000 --- a/error-boundaries/tests/similar-name-no-op/expected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; - -// Should NOT transform - different identifier -class Component extends React.Component { - unstable_handleErrorX() {} - handleError() {} - render() { - return null; - } -} diff --git a/error-boundaries/tests/similar-name-no-op/input.tsx b/error-boundaries/tests/similar-name-no-op/input.tsx deleted file mode 100644 index 336384f..0000000 --- a/error-boundaries/tests/similar-name-no-op/input.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; - -// Should NOT transform - different identifier -class Component extends React.Component { - unstable_handleErrorX() {} - handleError() {} - render() { - return null; - } -} diff --git a/error-boundaries/tests/single-method/expected.tsx b/error-boundaries/tests/single-method/expected.tsx deleted file mode 100644 index 8962fc4..0000000 --- a/error-boundaries/tests/single-method/expected.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; - -class ErrorBoundary extends React.Component { - componentDidCatch(error) { - logError(error); - } - render() { - return this.props.children; - } -} - -export default ErrorBoundary; diff --git a/error-boundaries/tests/single-method/input.tsx b/error-boundaries/tests/single-method/input.tsx deleted file mode 100644 index 326fd48..0000000 --- a/error-boundaries/tests/single-method/input.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; - -class ErrorBoundary extends React.Component { - unstable_handleError(error) { - logError(error); - } - render() { - return this.props.children; - } -} - -export default ErrorBoundary; diff --git a/error-boundaries/tests/single-method/metrics.json b/error-boundaries/tests/single-method/metrics.json deleted file mode 100644 index ffd4308..0000000 --- a/error-boundaries/tests/single-method/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error-boundary-renames": [ - { - "cardinality": { - "file": "tests/single-method/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/error-boundaries/tests/three-classes/expected.tsx b/error-boundaries/tests/three-classes/expected.tsx deleted file mode 100644 index df79ec7..0000000 --- a/error-boundaries/tests/three-classes/expected.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; - -class A extends React.Component { - componentDidCatch() {} - render() { - return null; - } -} - -class B extends React.Component { - componentDidCatch() {} - render() { - return null; - } -} - -class C extends React.Component { - componentDidCatch() {} - render() { - return null; - } -} diff --git a/error-boundaries/tests/three-classes/input.tsx b/error-boundaries/tests/three-classes/input.tsx deleted file mode 100644 index 8133110..0000000 --- a/error-boundaries/tests/three-classes/input.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; - -class A extends React.Component { - unstable_handleError() {} - render() { - return null; - } -} - -class B extends React.Component { - unstable_handleError() {} - render() { - return null; - } -} - -class C extends React.Component { - unstable_handleError() {} - render() { - return null; - } -} diff --git a/error-boundaries/tests/three-classes/metrics.json b/error-boundaries/tests/three-classes/metrics.json deleted file mode 100644 index 4f7b001..0000000 --- a/error-boundaries/tests/three-classes/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error-boundary-renames": [ - { - "cardinality": { - "file": "tests/three-classes/input.tsx" - }, - "count": 3 - } - ] -} \ No newline at end of file diff --git a/error-boundaries/tests/typescript-method/expected.tsx b/error-boundaries/tests/typescript-method/expected.tsx deleted file mode 100644 index 85e2fb1..0000000 --- a/error-boundaries/tests/typescript-method/expected.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; - -interface Props { - children: React.ReactNode; -} - -export class TypedErrorBoundary extends React.Component { - componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { - console.error(error, errorInfo); - } - - render() { - return this.props.children; - } -} diff --git a/error-boundaries/tests/typescript-method/input.tsx b/error-boundaries/tests/typescript-method/input.tsx deleted file mode 100644 index f248169..0000000 --- a/error-boundaries/tests/typescript-method/input.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; - -interface Props { - children: React.ReactNode; -} - -export class TypedErrorBoundary extends React.Component { - unstable_handleError(error: Error, errorInfo: React.ErrorInfo): void { - console.error(error, errorInfo); - } - - render() { - return this.props.children; - } -} diff --git a/error-boundaries/tests/typescript-method/metrics.json b/error-boundaries/tests/typescript-method/metrics.json deleted file mode 100644 index d9a41ea..0000000 --- a/error-boundaries/tests/typescript-method/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error-boundary-renames": [ - { - "cardinality": { - "file": "tests/typescript-method/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/find-dom-node/tests/comment-and-string-unchanged/expected.tsx b/find-dom-node/tests/comment-and-string-unchanged/expected.tsx deleted file mode 100644 index 9e29801..0000000 --- a/find-dom-node/tests/comment-and-string-unchanged/expected.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; - -// Migrate getDOMNode() to React.findDOMNode() when you run the codemod. -const LEGACY_API = "getDOMNode"; - -class Component extends React.Component { - componentDidMount() { - // We use this.refs.div for something else, no getDOMNode here - this.refs.div.classList.add("mounted"); - } - - render() { - return
; - } -} - -export default Component; diff --git a/find-dom-node/tests/comment-and-string-unchanged/input.tsx b/find-dom-node/tests/comment-and-string-unchanged/input.tsx deleted file mode 100644 index 9e29801..0000000 --- a/find-dom-node/tests/comment-and-string-unchanged/input.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; - -// Migrate getDOMNode() to React.findDOMNode() when you run the codemod. -const LEGACY_API = "getDOMNode"; - -class Component extends React.Component { - componentDidMount() { - // We use this.refs.div for something else, no getDOMNode here - this.refs.div.classList.add("mounted"); - } - - render() { - return
; - } -} - -export default Component; diff --git a/find-dom-node/tests/enzyme-wrapper-style/expected.tsx b/find-dom-node/tests/enzyme-wrapper-style/expected.tsx deleted file mode 100644 index 3b04af4..0000000 --- a/find-dom-node/tests/enzyme-wrapper-style/expected.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import { mount } from "enzyme"; -import MyComponent from "./MyComponent"; - -describe("MyComponent", () => { - it("focuses the input on mount", () => { - const wrapper = mount(); - const inputNode = React.findDOMNode(wrapper.find("input")); - expect(inputNode).toBe(document.activeElement); - }); - - it("calls getDOMNode on wrapper for measurement", () => { - const wrapper = mount(); - const domNode = React.findDOMNode(wrapper); - expect(domNode).toBeInstanceOf(HTMLElement); - }); -}); diff --git a/find-dom-node/tests/enzyme-wrapper-style/input.tsx b/find-dom-node/tests/enzyme-wrapper-style/input.tsx deleted file mode 100644 index ccff91d..0000000 --- a/find-dom-node/tests/enzyme-wrapper-style/input.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import { mount } from "enzyme"; -import MyComponent from "./MyComponent"; - -describe("MyComponent", () => { - it("focuses the input on mount", () => { - const wrapper = mount(); - const inputNode = wrapper.find("input").getDOMNode(); - expect(inputNode).toBe(document.activeElement); - }); - - it("calls getDOMNode on wrapper for measurement", () => { - const wrapper = mount(); - const domNode = wrapper.getDOMNode(); - expect(domNode).toBeInstanceOf(HTMLElement); - }); -}); diff --git a/find-dom-node/tests/enzyme-wrapper-style/metrics.json b/find-dom-node/tests/enzyme-wrapper-style/metrics.json deleted file mode 100644 index f5bd3ae..0000000 --- a/find-dom-node/tests/enzyme-wrapper-style/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/enzyme-wrapper-style/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/find-dom-node/tests/inside-another-call/expected.tsx b/find-dom-node/tests/inside-another-call/expected.tsx deleted file mode 100644 index c4c19c8..0000000 --- a/find-dom-node/tests/inside-another-call/expected.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; - -class SearchInput extends React.Component { - componentDidMount() { - // Real pattern: focus the input after mount or scroll it into view - const el = React.findDOMNode(this.refs.input); - if (el && typeof el.focus === "function") { - el.focus(); - } - } - - handleSubmit = () => { - const node = React.findDOMNode(this); - if (node) { - node.scrollIntoView({ behavior: "smooth" }); - } - }; - - render() { - return ; - } -} - -export default SearchInput; diff --git a/find-dom-node/tests/inside-another-call/input.tsx b/find-dom-node/tests/inside-another-call/input.tsx deleted file mode 100644 index ccc3bd7..0000000 --- a/find-dom-node/tests/inside-another-call/input.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; - -class SearchInput extends React.Component { - componentDidMount() { - // Real pattern: focus the input after mount or scroll it into view - const el = this.refs.input.getDOMNode(); - if (el && typeof el.focus === "function") { - el.focus(); - } - } - - handleSubmit = () => { - const node = this.getDOMNode(); - if (node) { - node.scrollIntoView({ behavior: "smooth" }); - } - }; - - render() { - return ; - } -} - -export default SearchInput; diff --git a/find-dom-node/tests/inside-another-call/metrics.json b/find-dom-node/tests/inside-another-call/metrics.json deleted file mode 100644 index e98fe22..0000000 --- a/find-dom-node/tests/inside-another-call/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/inside-another-call/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/find-dom-node/tests/multiple-calls/expected.tsx b/find-dom-node/tests/multiple-calls/expected.tsx deleted file mode 100644 index 7c9ce4b..0000000 --- a/find-dom-node/tests/multiple-calls/expected.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; - -class Component extends React.Component { - handleClick = () => { - const a = React.findDOMNode(this); - const b = React.findDOMNode(this.refs.foo); - a.classList.add("x"); - b.classList.add("y"); - }; - render() { - return
; - } -} diff --git a/find-dom-node/tests/multiple-calls/input.tsx b/find-dom-node/tests/multiple-calls/input.tsx deleted file mode 100644 index c04d12a..0000000 --- a/find-dom-node/tests/multiple-calls/input.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; - -class Component extends React.Component { - handleClick = () => { - const a = this.getDOMNode(); - const b = this.refs.foo.getDOMNode(); - a.classList.add("x"); - b.classList.add("y"); - }; - render() { - return
; - } -} diff --git a/find-dom-node/tests/multiple-calls/metrics.json b/find-dom-node/tests/multiple-calls/metrics.json deleted file mode 100644 index f6c3891..0000000 --- a/find-dom-node/tests/multiple-calls/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/multiple-calls/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/find-dom-node/tests/nested-refs/expected.tsx b/find-dom-node/tests/nested-refs/expected.tsx deleted file mode 100644 index 381c718..0000000 --- a/find-dom-node/tests/nested-refs/expected.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; - -interface Props { - onMeasure?: (node: Element | null) => void; -} - -class NestedLayout extends React.Component { - componentDidMount() { - // Legacy pattern: nested refs for list inside a main container - const listNode = React.findDOMNode(this.refs.main.refs.list); - this.props.onMeasure?.(listNode as Element); - } - - render() { - return ( -
-
-
    -
  • Item 1
  • -
  • Item 2
  • -
-
-
- ); - } -} - -export default NestedLayout; diff --git a/find-dom-node/tests/nested-refs/input.tsx b/find-dom-node/tests/nested-refs/input.tsx deleted file mode 100644 index b05676d..0000000 --- a/find-dom-node/tests/nested-refs/input.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; - -interface Props { - onMeasure?: (node: Element | null) => void; -} - -class NestedLayout extends React.Component { - componentDidMount() { - // Legacy pattern: nested refs for list inside a main container - const listNode = this.refs.main.refs.list.getDOMNode(); - this.props.onMeasure?.(listNode as Element); - } - - render() { - return ( -
-
-
    -
  • Item 1
  • -
  • Item 2
  • -
-
-
- ); - } -} - -export default NestedLayout; diff --git a/find-dom-node/tests/no-getDOMNode/expected.tsx b/find-dom-node/tests/no-getDOMNode/expected.tsx deleted file mode 100644 index dd2968d..0000000 --- a/find-dom-node/tests/no-getDOMNode/expected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; - -class Component extends React.Component { - componentDidMount() { - this.refs.div.focus(); - } - render() { - return
; - } -} diff --git a/find-dom-node/tests/no-getDOMNode/input.tsx b/find-dom-node/tests/no-getDOMNode/input.tsx deleted file mode 100644 index dd2968d..0000000 --- a/find-dom-node/tests/no-getDOMNode/input.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; - -class Component extends React.Component { - componentDidMount() { - this.refs.div.focus(); - } - render() { - return
; - } -} diff --git a/find-dom-node/tests/react-import-alias/expected.tsx b/find-dom-node/tests/react-import-alias/expected.tsx deleted file mode 100644 index 9d18394..0000000 --- a/find-dom-node/tests/react-import-alias/expected.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import R from "react"; - -class LegacyComponent extends R.Component { - componentDidMount() { - const el = R.findDOMNode(this); - if (el) (el as HTMLElement).focus(); - } - - render() { - return
; - } -} - -export default LegacyComponent; diff --git a/find-dom-node/tests/react-import-alias/input.tsx b/find-dom-node/tests/react-import-alias/input.tsx deleted file mode 100644 index a21afa1..0000000 --- a/find-dom-node/tests/react-import-alias/input.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import R from "react"; - -class LegacyComponent extends R.Component { - componentDidMount() { - const el = this.getDOMNode(); - if (el) (el as HTMLElement).focus(); - } - - render() { - return
; - } -} - -export default LegacyComponent; diff --git a/find-dom-node/tests/react-import-alias/metrics.json b/find-dom-node/tests/react-import-alias/metrics.json deleted file mode 100644 index 8599294..0000000 --- a/find-dom-node/tests/react-import-alias/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/react-import-alias/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/find-dom-node/tests/real-world-autosize-input/expected.tsx b/find-dom-node/tests/real-world-autosize-input/expected.tsx deleted file mode 100644 index 81357c0..0000000 --- a/find-dom-node/tests/real-world-autosize-input/expected.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -class AutosizeInput extends React.Component { - componentDidMount() { - const el = React.findDOMNode(this.refs.input); - if (el && typeof el.focus === "function") { - el.focus(); - } - this.copyStyles(); - } - - copyStyles() { - const node = React.findDOMNode(this); - if (node && this.props.styles) { - const target = node as HTMLElement; - Object.assign(target.style, this.props.styles); - } - } - - render() { - return ; - } -} - -export default AutosizeInput; diff --git a/find-dom-node/tests/real-world-autosize-input/input.tsx b/find-dom-node/tests/real-world-autosize-input/input.tsx deleted file mode 100644 index 808cefc..0000000 --- a/find-dom-node/tests/real-world-autosize-input/input.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -class AutosizeInput extends React.Component { - componentDidMount() { - const el = this.refs.input.getDOMNode(); - if (el && typeof el.focus === "function") { - el.focus(); - } - this.copyStyles(); - } - - copyStyles() { - const node = this.getDOMNode(); - if (node && this.props.styles) { - const target = node as HTMLElement; - Object.assign(target.style, this.props.styles); - } - } - - render() { - return ; - } -} - -export default AutosizeInput; diff --git a/find-dom-node/tests/real-world-autosize-input/metrics.json b/find-dom-node/tests/real-world-autosize-input/metrics.json deleted file mode 100644 index 239a1c4..0000000 --- a/find-dom-node/tests/real-world-autosize-input/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/real-world-autosize-input/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/find-dom-node/tests/refs-getDOMNode/expected.tsx b/find-dom-node/tests/refs-getDOMNode/expected.tsx deleted file mode 100644 index f6b70b9..0000000 --- a/find-dom-node/tests/refs-getDOMNode/expected.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; - -class Form extends React.Component { - submit() { - const input = React.findDOMNode(this.refs.emailInput); - input.focus(); - } - render() { - return ( -
- -
- ); - } -} diff --git a/find-dom-node/tests/refs-getDOMNode/input.tsx b/find-dom-node/tests/refs-getDOMNode/input.tsx deleted file mode 100644 index c99cdd4..0000000 --- a/find-dom-node/tests/refs-getDOMNode/input.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; - -class Form extends React.Component { - submit() { - const input = this.refs.emailInput.getDOMNode(); - input.focus(); - } - render() { - return ( -
- -
- ); - } -} diff --git a/find-dom-node/tests/refs-getDOMNode/metrics.json b/find-dom-node/tests/refs-getDOMNode/metrics.json deleted file mode 100644 index 0ab6312..0000000 --- a/find-dom-node/tests/refs-getDOMNode/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/refs-getDOMNode/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/find-dom-node/tests/this-getDOMNode/expected.tsx b/find-dom-node/tests/this-getDOMNode/expected.tsx deleted file mode 100644 index 5f32477..0000000 --- a/find-dom-node/tests/this-getDOMNode/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -class MyComponent extends React.Component { - componentDidMount() { - const node = React.findDOMNode(this); - node.focus(); - } - render() { - return
; - } -} diff --git a/find-dom-node/tests/this-getDOMNode/input.tsx b/find-dom-node/tests/this-getDOMNode/input.tsx deleted file mode 100644 index 1223434..0000000 --- a/find-dom-node/tests/this-getDOMNode/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -class MyComponent extends React.Component { - componentDidMount() { - const node = this.getDOMNode(); - node.focus(); - } - render() { - return
; - } -} diff --git a/find-dom-node/tests/this-getDOMNode/metrics.json b/find-dom-node/tests/this-getDOMNode/metrics.json deleted file mode 100644 index ec124fe..0000000 --- a/find-dom-node/tests/this-getDOMNode/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/this-getDOMNode/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/find-dom-node/tests/two-components-same-file/expected.tsx b/find-dom-node/tests/two-components-same-file/expected.tsx deleted file mode 100644 index 8c409d5..0000000 --- a/find-dom-node/tests/two-components-same-file/expected.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; - -class Modal extends React.Component { - componentDidMount() { - const node = React.findDOMNode(this); - if (node) { - (node as HTMLElement).setAttribute("aria-hidden", "false"); - } - } - - render() { - return
{this.props.children}
; - } -} - -class Tooltip extends React.Component<{ targetRef?: React.ReactInstance }> { - position() { - const tipNode = React.findDOMNode(this.refs.tip); - const target = this.props.targetRef; - // ... positioning logic using tipNode - return tipNode; - } - - render() { - return
; - } -} - -export { Modal, Tooltip }; diff --git a/find-dom-node/tests/two-components-same-file/input.tsx b/find-dom-node/tests/two-components-same-file/input.tsx deleted file mode 100644 index 8825f9f..0000000 --- a/find-dom-node/tests/two-components-same-file/input.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; - -class Modal extends React.Component { - componentDidMount() { - const node = this.getDOMNode(); - if (node) { - (node as HTMLElement).setAttribute("aria-hidden", "false"); - } - } - - render() { - return
{this.props.children}
; - } -} - -class Tooltip extends React.Component<{ targetRef?: React.ReactInstance }> { - position() { - const tipNode = this.refs.tip.getDOMNode(); - const target = this.props.targetRef; - // ... positioning logic using tipNode - return tipNode; - } - - render() { - return
; - } -} - -export { Modal, Tooltip }; diff --git a/find-dom-node/tests/two-components-same-file/metrics.json b/find-dom-node/tests/two-components-same-file/metrics.json deleted file mode 100644 index 17b1a38..0000000 --- a/find-dom-node/tests/two-components-same-file/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/two-components-same-file/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/find-dom-node/tests/variable-holding-ref/expected.tsx b/find-dom-node/tests/variable-holding-ref/expected.tsx deleted file mode 100644 index c5c6294..0000000 --- a/find-dom-node/tests/variable-holding-ref/expected.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; - -class FormWithRefs extends React.Component { - submitForm = () => { - const emailInput = this.refs.email; - const emailNode = React.findDOMNode(emailInput); - if (emailNode && (emailNode as HTMLInputElement).value.trim() === "") { - (emailNode as HTMLInputElement).focus(); - return; - } - // ... submit logic - }; - - render() { - return ( -
- - -
- ); - } -} - -export default FormWithRefs; diff --git a/find-dom-node/tests/variable-holding-ref/input.tsx b/find-dom-node/tests/variable-holding-ref/input.tsx deleted file mode 100644 index 9261aac..0000000 --- a/find-dom-node/tests/variable-holding-ref/input.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from "react"; - -class FormWithRefs extends React.Component { - submitForm = () => { - const emailInput = this.refs.email; - const emailNode = emailInput.getDOMNode(); - if (emailNode && (emailNode as HTMLInputElement).value.trim() === "") { - (emailNode as HTMLInputElement).focus(); - return; - } - // ... submit logic - }; - - render() { - return ( -
- - -
- ); - } -} - -export default FormWithRefs; diff --git a/find-dom-node/tests/variable-holding-ref/metrics.json b/find-dom-node/tests/variable-holding-ref/metrics.json deleted file mode 100644 index 8d1681b..0000000 --- a/find-dom-node/tests/variable-holding-ref/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/variable-holding-ref/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/manual-bind-to-arrow/tests/bind-outside-constructor-no-op/expected.tsx b/manual-bind-to-arrow/tests/bind-outside-constructor-no-op/expected.tsx deleted file mode 100644 index a4cb0c2..0000000 --- a/manual-bind-to-arrow/tests/bind-outside-constructor-no-op/expected.tsx +++ /dev/null @@ -1,13 +0,0 @@ -class BadExample extends React.Component { - componentDidMount() { - this.handleClick = this.handleClick.bind(this); - } - - handleClick() { - this.setState({ clicked: true }); - } - - render() { - return ; - } -} diff --git a/manual-bind-to-arrow/tests/bind-outside-constructor-no-op/input.tsx b/manual-bind-to-arrow/tests/bind-outside-constructor-no-op/input.tsx deleted file mode 100644 index a4cb0c2..0000000 --- a/manual-bind-to-arrow/tests/bind-outside-constructor-no-op/input.tsx +++ /dev/null @@ -1,13 +0,0 @@ -class BadExample extends React.Component { - componentDidMount() { - this.handleClick = this.handleClick.bind(this); - } - - handleClick() { - this.setState({ clicked: true }); - } - - render() { - return ; - } -} diff --git a/manual-bind-to-arrow/tests/bind-with-other-constructor-code/expected.tsx b/manual-bind-to-arrow/tests/bind-with-other-constructor-code/expected.tsx deleted file mode 100644 index 6d68949..0000000 --- a/manual-bind-to-arrow/tests/bind-with-other-constructor-code/expected.tsx +++ /dev/null @@ -1,14 +0,0 @@ -class DataTable extends React.Component { - constructor(props) { - super(props); - this.state = { sortKey: null }; - } - - handleSort = (column) => { - this.setState({ sortKey: column }); - } - - render() { - return ; - } -} diff --git a/manual-bind-to-arrow/tests/bind-with-other-constructor-code/input.tsx b/manual-bind-to-arrow/tests/bind-with-other-constructor-code/input.tsx deleted file mode 100644 index 8227c22..0000000 --- a/manual-bind-to-arrow/tests/bind-with-other-constructor-code/input.tsx +++ /dev/null @@ -1,15 +0,0 @@ -class DataTable extends React.Component { - constructor(props) { - super(props); - this.state = { sortKey: null }; - this.handleSort = this.handleSort.bind(this); - } - - handleSort(column) { - this.setState({ sortKey: column }); - } - - render() { - return
; - } -} diff --git a/manual-bind-to-arrow/tests/constructor-with-props/expected.tsx b/manual-bind-to-arrow/tests/constructor-with-props/expected.tsx deleted file mode 100644 index 9bea035..0000000 --- a/manual-bind-to-arrow/tests/constructor-with-props/expected.tsx +++ /dev/null @@ -1,13 +0,0 @@ -class Pagination extends React.Component { - onPageChange = (page) => { - this.props.onChange(page); - } - - render() { - return ( -
- -
- ); - } -} diff --git a/manual-bind-to-arrow/tests/constructor-with-props/input.tsx b/manual-bind-to-arrow/tests/constructor-with-props/input.tsx deleted file mode 100644 index b65a227..0000000 --- a/manual-bind-to-arrow/tests/constructor-with-props/input.tsx +++ /dev/null @@ -1,18 +0,0 @@ -class Pagination extends React.Component { - constructor(props) { - super(props); - this.onPageChange = this.onPageChange.bind(this); - } - - onPageChange(page) { - this.props.onChange(page); - } - - render() { - return ( -
- -
- ); - } -} diff --git a/manual-bind-to-arrow/tests/no-bind/expected.tsx b/manual-bind-to-arrow/tests/no-bind/expected.tsx deleted file mode 100644 index d185609..0000000 --- a/manual-bind-to-arrow/tests/no-bind/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ -class Component extends React.Component { - render() { - return
; - } - handleClick() {} -} diff --git a/manual-bind-to-arrow/tests/no-bind/input.tsx b/manual-bind-to-arrow/tests/no-bind/input.tsx deleted file mode 100644 index d185609..0000000 --- a/manual-bind-to-arrow/tests/no-bind/input.tsx +++ /dev/null @@ -1,6 +0,0 @@ -class Component extends React.Component { - render() { - return
; - } - handleClick() {} -} diff --git a/manual-bind-to-arrow/tests/real-world-dropdown/expected.tsx b/manual-bind-to-arrow/tests/real-world-dropdown/expected.tsx deleted file mode 100644 index 696661e..0000000 --- a/manual-bind-to-arrow/tests/real-world-dropdown/expected.tsx +++ /dev/null @@ -1,28 +0,0 @@ -class Dropdown extends React.Component { - componentDidMount() { - document.addEventListener('click', this.handleOutsideClick); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleOutsideClick); - } - - toggle = () => { - this.setState({ open: !this.state.open }); - } - - handleOutsideClick = (e) => { - if (this.refs.dropdown && !this.refs.dropdown.contains(e.target)) { - this.setState({ open: false }); - } - } - - render() { - return ( -
- - {this.state.open &&
    {this.props.children}
} -
- ); - } -} diff --git a/manual-bind-to-arrow/tests/real-world-dropdown/input.tsx b/manual-bind-to-arrow/tests/real-world-dropdown/input.tsx deleted file mode 100644 index 22b4c8a..0000000 --- a/manual-bind-to-arrow/tests/real-world-dropdown/input.tsx +++ /dev/null @@ -1,34 +0,0 @@ -class Dropdown extends React.Component { - constructor() { - super(); - this.toggle = this.toggle.bind(this); - this.handleOutsideClick = this.handleOutsideClick.bind(this); - } - - componentDidMount() { - document.addEventListener('click', this.handleOutsideClick); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleOutsideClick); - } - - toggle() { - this.setState({ open: !this.state.open }); - } - - handleOutsideClick(e) { - if (this.refs.dropdown && !this.refs.dropdown.contains(e.target)) { - this.setState({ open: false }); - } - } - - render() { - return ( -
- - {this.state.open &&
    {this.props.children}
} -
- ); - } -} diff --git a/manual-bind-to-arrow/tests/real-world-form-two-binds/expected.tsx b/manual-bind-to-arrow/tests/real-world-form-two-binds/expected.tsx deleted file mode 100644 index 138b037..0000000 --- a/manual-bind-to-arrow/tests/real-world-form-two-binds/expected.tsx +++ /dev/null @@ -1,20 +0,0 @@ -class ContactForm extends React.Component { - handleSubmit = (e) => { - e.preventDefault(); - this.props.onSubmit(this.state); - } - - handleChange = (e) => { - this.setState({ [e.target.name]: e.target.value }); - } - - render() { - return ( -
- - - - - ); - } -} diff --git a/manual-bind-to-arrow/tests/real-world-form-two-binds/input.tsx b/manual-bind-to-arrow/tests/real-world-form-two-binds/input.tsx deleted file mode 100644 index a3e9059..0000000 --- a/manual-bind-to-arrow/tests/real-world-form-two-binds/input.tsx +++ /dev/null @@ -1,26 +0,0 @@ -class ContactForm extends React.Component { - constructor(props) { - super(props); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleChange = this.handleChange.bind(this); - } - - handleSubmit(e) { - e.preventDefault(); - this.props.onSubmit(this.state); - } - - handleChange(e) { - this.setState({ [e.target.name]: e.target.value }); - } - - render() { - return ( -
- - - - - ); - } -} diff --git a/manual-bind-to-arrow/tests/real-world-form-two-binds/metrics.json b/manual-bind-to-arrow/tests/real-world-form-two-binds/metrics.json deleted file mode 100644 index 3dd8706..0000000 --- a/manual-bind-to-arrow/tests/real-world-form-two-binds/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/real-world-form-two-binds/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/manual-bind-to-arrow/tests/real-world-todo-item/expected.tsx b/manual-bind-to-arrow/tests/real-world-todo-item/expected.tsx deleted file mode 100644 index 8e632b2..0000000 --- a/manual-bind-to-arrow/tests/real-world-todo-item/expected.tsx +++ /dev/null @@ -1,20 +0,0 @@ -class TodoItem extends React.Component { - handleToggle = () => { - this.props.onToggle(this.props.id); - } - - handleDelete = () => { - this.props.onDelete(this.props.id); - } - - render() { - const { completed, text } = this.props; - return ( -
  • - - {text} - -
  • - ); - } -} diff --git a/manual-bind-to-arrow/tests/real-world-todo-item/input.tsx b/manual-bind-to-arrow/tests/real-world-todo-item/input.tsx deleted file mode 100644 index 4f63382..0000000 --- a/manual-bind-to-arrow/tests/real-world-todo-item/input.tsx +++ /dev/null @@ -1,26 +0,0 @@ -class TodoItem extends React.Component { - constructor(props) { - super(props); - this.handleToggle = this.handleToggle.bind(this); - this.handleDelete = this.handleDelete.bind(this); - } - - handleToggle() { - this.props.onToggle(this.props.id); - } - - handleDelete() { - this.props.onDelete(this.props.id); - } - - render() { - const { completed, text } = this.props; - return ( -
  • - - {text} - -
  • - ); - } -} diff --git a/manual-bind-to-arrow/tsconfig.json b/manual-bind-to-arrow/tsconfig.json deleted file mode 100644 index 9b4220a..0000000 --- a/manual-bind-to-arrow/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], - "allowImportingTsExtensions": true, - "noEmit": true, - "verbatimModuleSyntax": true, - "strict": true - }, - "exclude": ["tests"] -} diff --git a/package.json b/package.json index 1d606ce..9cfee3b 100644 --- a/package.json +++ b/package.json @@ -6,19 +6,19 @@ "type": "module", "scripts": { "test": "pnpm -r test", - "test:use-context-hook": "pnpm --filter @react-codemods/use-context-hook test", - "test:update-react-imports": "pnpm --filter @react-codemods/update-react-imports test", - "test:replace-reactdom-render": "pnpm --filter @react-codemods/replace-reactdom-render test", - "test:replace-string-ref": "pnpm --filter @react-codemods/replace-string-ref test", - "test:rename-unsafe-lifecycles": "pnpm --filter @react-codemods/rename-unsafe-lifecycles test", - "test:error-boundaries": "pnpm --filter @react-codemods/error-boundaries test", - "test:replace-act-import": "pnpm --filter @react-codemods/replace-act-import test", - "test:replace-use-form-state": "pnpm --filter @react-codemods/replace-use-form-state test", - "test:find-dom-node": "pnpm --filter @react-codemods/find-dom-node test", - "test:react-proptypes-to-prop-types": "pnpm --filter @react-codemods/react-proptypes-to-prop-types test", - "test:pure-render-mixin": "pnpm --filter @react-codemods/pure-render-mixin test", - "test:sort-comp": "pnpm --filter @react-codemods/sort-comp test", - "test:manual-bind-to-arrow": "pnpm --filter @react-codemods/manual-bind-to-arrow test", + "test:use-context-hook": "pnpm --filter @react/use-context-hook test", + "test:update-react-imports": "pnpm --filter @react/update-react-imports test", + "test:replace-reactdom-render": "pnpm --filter @react/replace-reactdom-render test", + "test:replace-string-ref": "pnpm --filter @react/replace-string-ref test", + "test:rename-unsafe-lifecycles": "pnpm --filter @react/rename-unsafe-lifecycles test", + "test:error-boundaries": "pnpm --filter @react/error-boundaries test", + "test:replace-act-import": "pnpm --filter @react/replace-act-import test", + "test:replace-use-form-state": "pnpm --filter @react/replace-use-form-state test", + "test:find-dom-node": "pnpm --filter @react/find-dom-node test", + "test:react-proptypes-to-prop-types": "pnpm --filter @react/react-proptypes-to-prop-types test", + "test:pure-render-mixin": "pnpm --filter @react/pure-render-mixin test", + "test:sort-comp": "pnpm --filter @react/sort-comp test", + "test:manual-bind-to-arrow": "pnpm --filter @react/manual-bind-to-arrow test", "check-types": "pnpm -r check-types" }, "keywords": [ @@ -33,6 +33,9 @@ "url": "https://github.com/codemod/jssg-react-codemods.git" }, "packageManager": "pnpm@9.14.2", + "devDependencies": { + "@types/node": "latest" + }, "dependencies": { "@jssg/utils": "^0.0.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ad23d7..a380164 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,17 +11,24 @@ importers: '@jssg/utils': specifier: ^0.0.2 version: 0.0.2 + devDependencies: + '@types/node': + specifier: latest + version: 25.5.2 - error-boundaries: + codemods/error-boundaries: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - find-dom-node: + codemods/find-dom-node: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -29,30 +36,39 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - manual-bind-to-arrow: + codemods/manual-bind-to-arrow: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - pure-render-mixin: + codemods/pure-render-mixin: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - react-proptypes-to-prop-types: + codemods/react-proptypes-to-prop-types: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -60,21 +76,27 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - rename-unsafe-lifecycles: + codemods/rename-unsafe-lifecycles: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - replace-act-import: + codemods/replace-act-import: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -82,12 +104,15 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - replace-reactdom-render: + codemods/replace-reactdom-render: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -95,12 +120,15 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - replace-string-ref: + codemods/replace-string-ref: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -108,12 +136,15 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - replace-use-form-state: + codemods/replace-use-form-state: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -121,21 +152,27 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - sort-comp: + codemods/sort-comp: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - update-react-imports: + codemods/update-react-imports: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -143,12 +180,15 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 - use-context-hook: + codemods/use-context-hook: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -156,28 +196,43 @@ importers: devDependencies: '@codemod.com/jssg-types': specifier: latest - version: 1.5.0 + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.5.2 typescript: specifier: latest - version: 5.9.3 + version: 6.0.2 packages: - '@codemod.com/jssg-types@1.5.0': - resolution: {integrity: sha512-zChRbxI3hBSGrAHnWlEzOw1FztLWMMiarwcr0Wbk0On4hmv7dVgoUqpIHfxb64mEMKJ5syTIKY3ZNd8DcFQa5w==} + '@codemod.com/jssg-types@1.5.1': + resolution: {integrity: sha512-pCngSoz7hpNiMMkoVi+b4uqy7cA2XuJqEaOx52KYN/25bOdzJ7IqU9DEMukOKnWyFt9iy3UBinayit1P1+9wXA==} '@jssg/utils@0.0.2': resolution: {integrity: sha512-eCQv5Xs9yfI6OKq2PQ8SyKsxPhdiaJY/XAFJmsZiVbsK5DIBBmGBA95CsyD4JFWXDKkNv6Q7ER/Kbp6/uWzB2w==} - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + '@types/node@25.5.2': + resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + + typescript@6.0.2: + resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + snapshots: - '@codemod.com/jssg-types@1.5.0': {} + '@codemod.com/jssg-types@1.5.1': {} '@jssg/utils@0.0.2': {} - typescript@5.9.3: {} + '@types/node@25.5.2': + dependencies: + undici-types: 7.18.2 + + typescript@6.0.2: {} + + undici-types@7.18.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d415562..83b59d2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,14 +1,2 @@ packages: - - "replace-act-import" - - "replace-use-form-state" - - "use-context-hook" - - "update-react-imports" - - "replace-reactdom-render" - - "replace-string-ref" - - "rename-unsafe-lifecycles" - - "manual-bind-to-arrow" - - "sort-comp" - - "pure-render-mixin" - - "react-proptypes-to-prop-types" - - "error-boundaries" - - "find-dom-node" + - "codemods/*" diff --git a/pure-render-mixin/tests/create-class-direct-call/input.tsx b/pure-render-mixin/tests/create-class-direct-call/input.tsx deleted file mode 100644 index 619b84a..0000000 --- a/pure-render-mixin/tests/create-class-direct-call/input.tsx +++ /dev/null @@ -1,13 +0,0 @@ -var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; -var createClass = React.createClass; - -var Sidebar = createClass({ - mixins: [PureRenderMixin], - - render: function() { - return ; - }, -}); - -module.exports = Sidebar; diff --git a/pure-render-mixin/tests/create-class-direct-call/metrics.json b/pure-render-mixin/tests/create-class-direct-call/metrics.json deleted file mode 100644 index e9a1503..0000000 --- a/pure-render-mixin/tests/create-class-direct-call/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/create-class-direct-call/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/pure-render-mixin/tests/no-pure-render-mixin/expected.tsx b/pure-render-mixin/tests/no-pure-render-mixin/expected.tsx deleted file mode 100644 index 1e98a17..0000000 --- a/pure-render-mixin/tests/no-pure-render-mixin/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -var React = require('react'); - -var MyComponent = React.createClass({ - mixins: [SomeOtherMixin], - - render: function() { - return
    ; - }, -}); - -module.exports = MyComponent; diff --git a/pure-render-mixin/tests/no-pure-render-mixin/input.tsx b/pure-render-mixin/tests/no-pure-render-mixin/input.tsx deleted file mode 100644 index 1e98a17..0000000 --- a/pure-render-mixin/tests/no-pure-render-mixin/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -var React = require('react'); - -var MyComponent = React.createClass({ - mixins: [SomeOtherMixin], - - render: function() { - return
    ; - }, -}); - -module.exports = MyComponent; diff --git a/pure-render-mixin/tests/original-fixture3-removes-var/metrics.json b/pure-render-mixin/tests/original-fixture3-removes-var/metrics.json deleted file mode 100644 index 97a421d..0000000 --- a/pure-render-mixin/tests/original-fixture3-removes-var/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/original-fixture3-removes-var/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/pure-render-mixin/tests/real-world-form/expected.tsx b/pure-render-mixin/tests/real-world-form/expected.tsx deleted file mode 100644 index 0c7106d..0000000 --- a/pure-render-mixin/tests/real-world-form/expected.tsx +++ /dev/null @@ -1,29 +0,0 @@ -var React = require("react/addons"); -var Form = React.createClass({ - getInitialState: function() { - return { values: {} }; - }, - - handleSubmit: function(e) { - e.preventDefault(); - this.props.onSubmit(this.state.values); - }, - - - shouldComponentUpdate: function(nextProps, nextState) { - - return React.addons.shallowCompare(this, nextProps, nextState); - - }, - - render: function() { - return ( -
    - - - - ); - }, -}); - -module.exports = Form; diff --git a/pure-render-mixin/tests/real-world-form/input.tsx b/pure-render-mixin/tests/real-world-form/input.tsx deleted file mode 100644 index 0f38fa4..0000000 --- a/pure-render-mixin/tests/real-world-form/input.tsx +++ /dev/null @@ -1,26 +0,0 @@ -var React = require("react/addons"); -var PureRenderMixin = React.addons.PureRenderMixin; - -var Form = React.createClass({ - mixins: [PureRenderMixin], - - getInitialState: function() { - return { values: {} }; - }, - - handleSubmit: function(e) { - e.preventDefault(); - this.props.onSubmit(this.state.values); - }, - - render: function() { - return ( -
    - - - - ); - }, -}); - -module.exports = Form; diff --git a/pure-render-mixin/tests/real-world-list-item/expected.tsx b/pure-render-mixin/tests/real-world-list-item/expected.tsx deleted file mode 100644 index 0b3ff2a..0000000 --- a/pure-render-mixin/tests/real-world-list-item/expected.tsx +++ /dev/null @@ -1,33 +0,0 @@ -var React = require('react/addons'); -var TodoItem = React.createClass({ - displayName: 'TodoItem', - - propTypes: { - id: React.PropTypes.string.isRequired, - title: React.PropTypes.string, - completed: React.PropTypes.bool, - onToggle: React.PropTypes.func, - }, - - handleClick: function() { - this.props.onToggle(this.props.id); - }, - - - shouldComponentUpdate: function(nextProps, nextState) { - - return React.addons.shallowCompare(this, nextProps, nextState); - - }, - - render: function() { - var className = this.props.completed ? 'todo-item completed' : 'todo-item'; - return ( -
  • - {this.props.title} -
  • - ); - }, -}); - -module.exports = TodoItem; diff --git a/pure-render-mixin/tests/real-world-list-item/input.tsx b/pure-render-mixin/tests/real-world-list-item/input.tsx deleted file mode 100644 index 4ae280f..0000000 --- a/pure-render-mixin/tests/real-world-list-item/input.tsx +++ /dev/null @@ -1,30 +0,0 @@ -var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; - -var TodoItem = React.createClass({ - displayName: 'TodoItem', - - propTypes: { - id: React.PropTypes.string.isRequired, - title: React.PropTypes.string, - completed: React.PropTypes.bool, - onToggle: React.PropTypes.func, - }, - - mixins: [PureRenderMixin], - - handleClick: function() { - this.props.onToggle(this.props.id); - }, - - render: function() { - var className = this.props.completed ? 'todo-item completed' : 'todo-item'; - return ( -
  • - {this.props.title} -
  • - ); - }, -}); - -module.exports = TodoItem; diff --git a/pure-render-mixin/tests/real-world-modal-with-other-mixin/expected.tsx b/pure-render-mixin/tests/real-world-modal-with-other-mixin/expected.tsx deleted file mode 100644 index 1c27cb5..0000000 --- a/pure-render-mixin/tests/real-world-modal-with-other-mixin/expected.tsx +++ /dev/null @@ -1,37 +0,0 @@ -var React = require('react/addons'); -var EventListenerMixin = require('react-event-listener-mixin'); - -var Modal = React.createClass({ - mixins: [EventListenerMixin], - - componentDidMount: function() { - this.addEventListener(document, 'keydown', this.handleKeyDown); - }, - - componentWillUnmount: function() { - this.removeEventListener(document, 'keydown', this.handleKeyDown); - }, - - handleKeyDown: function(e) { - if (e.key === 'Escape') this.props.onClose(); - }, - - - shouldComponentUpdate: function(nextProps, nextState) { - - return React.addons.shallowCompare(this, nextProps, nextState); - - }, - - render: function() { - return ( -
    -
    - {this.props.children} -
    -
    - ); - }, -}); - -module.exports = Modal; diff --git a/pure-render-mixin/tests/real-world-modal-with-other-mixin/input.tsx b/pure-render-mixin/tests/real-world-modal-with-other-mixin/input.tsx deleted file mode 100644 index 31b1da4..0000000 --- a/pure-render-mixin/tests/real-world-modal-with-other-mixin/input.tsx +++ /dev/null @@ -1,31 +0,0 @@ -var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; -var EventListenerMixin = require('react-event-listener-mixin'); - -var Modal = React.createClass({ - mixins: [PureRenderMixin, EventListenerMixin], - - componentDidMount: function() { - this.addEventListener(document, 'keydown', this.handleKeyDown); - }, - - componentWillUnmount: function() { - this.removeEventListener(document, 'keydown', this.handleKeyDown); - }, - - handleKeyDown: function(e) { - if (e.key === 'Escape') this.props.onClose(); - }, - - render: function() { - return ( -
    -
    - {this.props.children} -
    -
    - ); - }, -}); - -module.exports = Modal; diff --git a/pure-render-mixin/tests/real-world-modal-with-other-mixin/metrics.json b/pure-render-mixin/tests/real-world-modal-with-other-mixin/metrics.json deleted file mode 100644 index b6b3fff..0000000 --- a/pure-render-mixin/tests/real-world-modal-with-other-mixin/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/real-world-modal-with-other-mixin/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/pure-render-mixin/tests/two-components-one-with-mixin/expected.tsx b/pure-render-mixin/tests/two-components-one-with-mixin/expected.tsx deleted file mode 100644 index 49d51b3..0000000 --- a/pure-render-mixin/tests/two-components-one-with-mixin/expected.tsx +++ /dev/null @@ -1,21 +0,0 @@ -var React = require('react/addons'); -var PlainComponent = React.createClass({ - render: function() { - return
    Plain
    ; - }, -}); - -var OptimizedComponent = React.createClass({ - - shouldComponentUpdate: function(nextProps, nextState) { - - return React.addons.shallowCompare(this, nextProps, nextState); - - }, - - render: function() { - return
    Optimized
    ; - }, -}); - -module.exports = { PlainComponent, OptimizedComponent }; diff --git a/pure-render-mixin/tests/two-components-one-with-mixin/input.tsx b/pure-render-mixin/tests/two-components-one-with-mixin/input.tsx deleted file mode 100644 index cc7145f..0000000 --- a/pure-render-mixin/tests/two-components-one-with-mixin/input.tsx +++ /dev/null @@ -1,18 +0,0 @@ -var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; - -var PlainComponent = React.createClass({ - render: function() { - return
    Plain
    ; - }, -}); - -var OptimizedComponent = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    Optimized
    ; - }, -}); - -module.exports = { PlainComponent, OptimizedComponent }; diff --git a/pure-render-mixin/tests/two-components-one-with-mixin/metrics.json b/pure-render-mixin/tests/two-components-one-with-mixin/metrics.json deleted file mode 100644 index 40a58eb..0000000 --- a/pure-render-mixin/tests/two-components-one-with-mixin/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/two-components-one-with-mixin/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/react-proptypes-to-prop-types/scripts/codemod.ts b/react-proptypes-to-prop-types/scripts/codemod.ts deleted file mode 100644 index d44a06a..0000000 --- a/react-proptypes-to-prop-types/scripts/codemod.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { Transform, Edit } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const edits: Edit[] = []; - - const propTypesMetric = useMetricAtom("react-proptypes-migrations"); - - const reactDefaultImport = getImport(rootNode, { type: "default", from: "react" }); - const reactName = reactDefaultImport?.alias ?? "React"; - - const propTypesImport = getImport(rootNode, { type: "default", from: "prop-types" }); - const propTypesName = propTypesImport?.alias ?? "PropTypes"; - - // Find member_expressions: React.PropTypes (object=React, property=PropTypes) - const reactPropTypesRefs = rootNode.findAll({ - rule: { - kind: "member_expression", - all: [ - { has: { field: "object", kind: "identifier", regex: `^${reactName}$` } }, - { has: { field: "property", kind: "property_identifier", regex: "^PropTypes$" } }, - ], - }, - }); - - for (const ref of reactPropTypesRefs) { - edits.push(ref.replace(propTypesName)); - propTypesMetric.increment({ - file: root.filename(), - }); - } - - if (edits.length === 0) return null; - - // Add prop-types import if not present (insert after first import) - if (!propTypesImport) { - const firstImport = rootNode.find({ - rule: { kind: "import_statement" }, - }); - if (firstImport) { - const importLine = `import ${propTypesName} from "prop-types";\n`; - const insertPos = firstImport.range().start.index; - edits.push({ startPos: insertPos, endPos: insertPos, insertedText: importLine }); - } - } - - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/react-proptypes-to-prop-types/tests/array-and-object/expected.tsx b/react-proptypes-to-prop-types/tests/array-and-object/expected.tsx deleted file mode 100644 index b855699..0000000 --- a/react-proptypes-to-prop-types/tests/array-and-object/expected.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from "prop-types"; -import React from "react"; - -Component.propTypes = { - items: PropTypes.array, - config: PropTypes.object, - id: PropTypes.oneOf([1, 2]).isRequired, -}; - -function Component() { - return null; -} diff --git a/react-proptypes-to-prop-types/tests/array-and-object/input.tsx b/react-proptypes-to-prop-types/tests/array-and-object/input.tsx deleted file mode 100644 index e301210..0000000 --- a/react-proptypes-to-prop-types/tests/array-and-object/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -Component.propTypes = { - items: React.PropTypes.array, - config: React.PropTypes.object, - id: React.PropTypes.oneOf([1, 2]).isRequired, -}; - -function Component() { - return null; -} diff --git a/react-proptypes-to-prop-types/tests/basic-prop-types/expected.tsx b/react-proptypes-to-prop-types/tests/basic-prop-types/expected.tsx deleted file mode 100644 index 019448e..0000000 --- a/react-proptypes-to-prop-types/tests/basic-prop-types/expected.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -const MyComponent = ({ name }: { name: string }) => ( -
    {name}
    -); - -MyComponent.propTypes = { - name: PropTypes.string, -}; - -export default MyComponent; diff --git a/react-proptypes-to-prop-types/tests/basic-prop-types/input.tsx b/react-proptypes-to-prop-types/tests/basic-prop-types/input.tsx deleted file mode 100644 index f23d333..0000000 --- a/react-proptypes-to-prop-types/tests/basic-prop-types/input.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; -import PropTypes from "prop-types"; - -const MyComponent = ({ name }: { name: string }) => ( -
    {name}
    -); - -MyComponent.propTypes = { - name: React.PropTypes.string, -}; - -export default MyComponent; diff --git a/react-proptypes-to-prop-types/tests/multiple-types/expected.tsx b/react-proptypes-to-prop-types/tests/multiple-types/expected.tsx deleted file mode 100644 index a61570b..0000000 --- a/react-proptypes-to-prop-types/tests/multiple-types/expected.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import PropTypes from "prop-types"; -import React from "react"; - -function Button({ label, disabled }: { label: string; disabled?: boolean }) { - return ; -} - -Button.propTypes = { - label: PropTypes.string.isRequired, - disabled: PropTypes.bool, -}; - -export default Button; diff --git a/react-proptypes-to-prop-types/tests/multiple-types/input.tsx b/react-proptypes-to-prop-types/tests/multiple-types/input.tsx deleted file mode 100644 index cf3cbc7..0000000 --- a/react-proptypes-to-prop-types/tests/multiple-types/input.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; - -function Button({ label, disabled }: { label: string; disabled?: boolean }) { - return ; -} - -Button.propTypes = { - label: React.PropTypes.string.isRequired, - disabled: React.PropTypes.bool, -}; - -export default Button; diff --git a/react-proptypes-to-prop-types/tests/no-react-proptypes/expected.tsx b/react-proptypes-to-prop-types/tests/no-react-proptypes/expected.tsx deleted file mode 100644 index 01042b1..0000000 --- a/react-proptypes-to-prop-types/tests/no-react-proptypes/expected.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import PropTypes from "prop-types"; - -const Component = () => null; -Component.propTypes = { id: PropTypes.string }; -export default Component; diff --git a/react-proptypes-to-prop-types/tests/no-react-proptypes/input.tsx b/react-proptypes-to-prop-types/tests/no-react-proptypes/input.tsx deleted file mode 100644 index 01042b1..0000000 --- a/react-proptypes-to-prop-types/tests/no-react-proptypes/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import PropTypes from "prop-types"; - -const Component = () => null; -Component.propTypes = { id: PropTypes.string }; -export default Component; diff --git a/rename-unsafe-lifecycles/README.md b/rename-unsafe-lifecycles/README.md deleted file mode 100644 index d290bda..0000000 --- a/rename-unsafe-lifecycles/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# @react-codemods/rename-unsafe-lifecycles - -Rename deprecated lifecycle methods to UNSAFE_ prefixed versions - -## Installation - -```bash -# Install from registry -codemod run @react-codemods/rename-unsafe-lifecycles - -# Or run locally -codemod run -w workflow.yaml -``` - -## Usage - -This codemod transforms tsx code by: - -- Converting `var` declarations to `const`/`let` -- Removing debug statements -- Modernizing syntax patterns - -## Development - -```bash -# Test the transformation -npm test - -# Validate the workflow -codemod validate -w workflow.yaml - -# Publish to registry -codemod login -codemod publish -``` - -## License -MIT diff --git a/rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx b/rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx deleted file mode 100644 index 857671c..0000000 --- a/rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - UNSAFE_componentWillMount = () => { - console.log("mounting"); - }; - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/arrow-functions/input.tsx b/rename-unsafe-lifecycles/tests/arrow-functions/input.tsx deleted file mode 100644 index e8ade10..0000000 --- a/rename-unsafe-lifecycles/tests/arrow-functions/input.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - componentWillMount = () => { - console.log("mounting"); - }; - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/class-methods/expected.tsx b/rename-unsafe-lifecycles/tests/class-methods/expected.tsx deleted file mode 100644 index 8481d72..0000000 --- a/rename-unsafe-lifecycles/tests/class-methods/expected.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - UNSAFE_componentWillMount() { - console.log("mounting"); - } - - UNSAFE_componentWillReceiveProps(nextProps) { - console.log("receiving props", nextProps); - } - - UNSAFE_componentWillUpdate(nextProps, nextState) { - console.log("updating", nextProps, nextState); - } - - componentDidMount() { - console.log("mounted"); - } - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/class-methods/input.tsx b/rename-unsafe-lifecycles/tests/class-methods/input.tsx deleted file mode 100644 index 89e873a..0000000 --- a/rename-unsafe-lifecycles/tests/class-methods/input.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - componentWillMount() { - console.log("mounting"); - } - - componentWillReceiveProps(nextProps) { - console.log("receiving props", nextProps); - } - - componentWillUpdate(nextProps, nextState) { - console.log("updating", nextProps, nextState); - } - - componentDidMount() { - console.log("mounted"); - } - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/componentWillUpdate-only/expected.tsx b/rename-unsafe-lifecycles/tests/componentWillUpdate-only/expected.tsx deleted file mode 100644 index 1afcf95..0000000 --- a/rename-unsafe-lifecycles/tests/componentWillUpdate-only/expected.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - UNSAFE_componentWillUpdate(nextProps: unknown, nextState: unknown) { - console.log("will update", nextProps, nextState); - } - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/componentWillUpdate-only/input.tsx b/rename-unsafe-lifecycles/tests/componentWillUpdate-only/input.tsx deleted file mode 100644 index 2a363ac..0000000 --- a/rename-unsafe-lifecycles/tests/componentWillUpdate-only/input.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - componentWillUpdate(nextProps: unknown, nextState: unknown) { - console.log("will update", nextProps, nextState); - } - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/componentWillUpdate-only/metrics.json b/rename-unsafe-lifecycles/tests/componentWillUpdate-only/metrics.json deleted file mode 100644 index e506b2a..0000000 --- a/rename-unsafe-lifecycles/tests/componentWillUpdate-only/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "lifecycle-renames": [ - { - "cardinality": { - "file": "tests/componentWillUpdate-only/input.tsx", - "lifecycles": "componentWillUpdate" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/rename-unsafe-lifecycles/tests/member-expression-call/expected.tsx b/rename-unsafe-lifecycles/tests/member-expression-call/expected.tsx deleted file mode 100644 index 25538a5..0000000 --- a/rename-unsafe-lifecycles/tests/member-expression-call/expected.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - UNSAFE_componentWillMount() { - console.log("mounting"); - } - - componentDidMount() { - this.UNSAFE_componentWillMount(); - } - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/member-expression-call/input.tsx b/rename-unsafe-lifecycles/tests/member-expression-call/input.tsx deleted file mode 100644 index 37b8883..0000000 --- a/rename-unsafe-lifecycles/tests/member-expression-call/input.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - componentWillMount() { - console.log("mounting"); - } - - componentDidMount() { - this.componentWillMount(); - } - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/no-deprecated-lifecycles/expected.tsx b/rename-unsafe-lifecycles/tests/no-deprecated-lifecycles/expected.tsx deleted file mode 100644 index 129b901..0000000 --- a/rename-unsafe-lifecycles/tests/no-deprecated-lifecycles/expected.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - componentDidMount() { - console.log("mounted"); - } - - componentDidUpdate(prevProps: unknown) { - console.log("updated", prevProps); - } - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/no-deprecated-lifecycles/input.tsx b/rename-unsafe-lifecycles/tests/no-deprecated-lifecycles/input.tsx deleted file mode 100644 index 129b901..0000000 --- a/rename-unsafe-lifecycles/tests/no-deprecated-lifecycles/input.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - componentDidMount() { - console.log("mounted"); - } - - componentDidUpdate(prevProps: unknown) { - console.log("updated", prevProps); - } - - render() { - return
    Hello
    ; - } -} - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/object-properties/expected.tsx b/rename-unsafe-lifecycles/tests/object-properties/expected.tsx deleted file mode 100644 index 45d7dbf..0000000 --- a/rename-unsafe-lifecycles/tests/object-properties/expected.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import createReactClass from "create-react-class"; - -const MyComponent = createReactClass({ - UNSAFE_componentWillMount: function() { - console.log("mounting"); - }, - - UNSAFE_componentWillReceiveProps: function(nextProps) { - console.log("receiving props", nextProps); - }, - - render: function() { - return
    Hello
    ; - } -}); - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tests/object-properties/input.tsx b/rename-unsafe-lifecycles/tests/object-properties/input.tsx deleted file mode 100644 index 37ce039..0000000 --- a/rename-unsafe-lifecycles/tests/object-properties/input.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import createReactClass from "create-react-class"; - -const MyComponent = createReactClass({ - componentWillMount: function() { - console.log("mounting"); - }, - - componentWillReceiveProps: function(nextProps) { - console.log("receiving props", nextProps); - }, - - render: function() { - return
    Hello
    ; - } -}); - -export default MyComponent; diff --git a/rename-unsafe-lifecycles/tsconfig.json b/rename-unsafe-lifecycles/tsconfig.json deleted file mode 100644 index 469fc5a..0000000 --- a/rename-unsafe-lifecycles/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], - "allowImportingTsExtensions": true, - "noEmit": true, - "verbatimModuleSyntax": true, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": ["tests"] -} diff --git a/replace-act-import/tests/act-in-async/expected.tsx b/replace-act-import/tests/act-in-async/expected.tsx deleted file mode 100644 index b1d1f91..0000000 --- a/replace-act-import/tests/act-in-async/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ - -it("handles async", async () => { - await act(async () => { - await doSomething(); - }); -}); diff --git a/replace-act-import/tests/act-in-async/input.tsx b/replace-act-import/tests/act-in-async/input.tsx deleted file mode 100644 index 2d255d1..0000000 --- a/replace-act-import/tests/act-in-async/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { act } from "react-dom/test-utils"; - -it("handles async", async () => { - await act(async () => { - await doSomething(); - }); -}); diff --git a/replace-act-import/tests/act-in-async/metrics.json b/replace-act-import/tests/act-in-async/metrics.json deleted file mode 100644 index 2680f9c..0000000 --- a/replace-act-import/tests/act-in-async/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace-act-import-migrations": [ - { - "cardinality": { - "file": "tests/act-in-async/input.tsx", - "pattern": "named-import" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-act-import/tests/export-named-act/expected.tsx b/replace-act-import/tests/export-named-act/expected.tsx deleted file mode 100644 index ce8f78b..0000000 --- a/replace-act-import/tests/export-named-act/expected.tsx +++ /dev/null @@ -1 +0,0 @@ -export { act } from "react"; diff --git a/replace-act-import/tests/export-named-act/input.tsx b/replace-act-import/tests/export-named-act/input.tsx deleted file mode 100644 index 2430a1e..0000000 --- a/replace-act-import/tests/export-named-act/input.tsx +++ /dev/null @@ -1 +0,0 @@ -export { act } from "react-dom/test-utils"; diff --git a/replace-act-import/tests/export-named-act/metrics.json b/replace-act-import/tests/export-named-act/metrics.json deleted file mode 100644 index d087f81..0000000 --- a/replace-act-import/tests/export-named-act/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace-act-import-migrations": [ - { - "cardinality": { - "file": "tests/export-named-act/input.tsx", - "pattern": "re-export" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-act-import/tests/multiple-act-calls/expected.tsx b/replace-act-import/tests/multiple-act-calls/expected.tsx deleted file mode 100644 index 00fb549..0000000 --- a/replace-act-import/tests/multiple-act-calls/expected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; - -test("example", async () => { - React.act(() => { - render(); - }); - React.act(() => { - fireEvent.click(button); - }); -}); diff --git a/replace-act-import/tests/multiple-act-calls/input.tsx b/replace-act-import/tests/multiple-act-calls/input.tsx deleted file mode 100644 index ad3e95e..0000000 --- a/replace-act-import/tests/multiple-act-calls/input.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import ReactTestUtils from "react-dom/test-utils"; - -test("example", async () => { - ReactTestUtils.act(() => { - render(); - }); - ReactTestUtils.act(() => { - fireEvent.click(button); - }); -}); diff --git a/replace-act-import/tests/multiple-act-calls/metrics.json b/replace-act-import/tests/multiple-act-calls/metrics.json deleted file mode 100644 index 6de8d82..0000000 --- a/replace-act-import/tests/multiple-act-calls/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace-act-import-migrations": [ - { - "cardinality": { - "file": "tests/multiple-act-calls/input.tsx", - "pattern": "member-call" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/replace-act-import/tests/named-import/expected.tsx b/replace-act-import/tests/named-import/expected.tsx deleted file mode 100644 index a49755f..0000000 --- a/replace-act-import/tests/named-import/expected.tsx +++ /dev/null @@ -1 +0,0 @@ -act(); diff --git a/replace-act-import/tests/react-testing-library-style/expected.tsx b/replace-act-import/tests/react-testing-library-style/expected.tsx deleted file mode 100644 index ff0e5d5..0000000 --- a/replace-act-import/tests/react-testing-library-style/expected.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; -import { renderHook } from "@testing-library/react"; -import { useCounter } from "../useCounter"; - -describe("useCounter", () => { - it("increments when dispatch is called", async () => { - const { result } = renderHook(() => useCounter(0)); - - await React.act(() => { - result.current.increment(); - }); - - expect(result.current.count).toBe(1); - }); -}); diff --git a/replace-act-import/tests/react-testing-library-style/input.tsx b/replace-act-import/tests/react-testing-library-style/input.tsx deleted file mode 100644 index 941b6c7..0000000 --- a/replace-act-import/tests/react-testing-library-style/input.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as ReactTestUtils from "react-dom/test-utils"; -import { renderHook } from "@testing-library/react"; -import { useCounter } from "../useCounter"; - -describe("useCounter", () => { - it("increments when dispatch is called", async () => { - const { result } = renderHook(() => useCounter(0)); - - await ReactTestUtils.act(() => { - result.current.increment(); - }); - - expect(result.current.count).toBe(1); - }); -}); diff --git a/replace-act-import/tests/react-testing-library-style/metrics.json b/replace-act-import/tests/react-testing-library-style/metrics.json deleted file mode 100644 index 0a1b02a..0000000 --- a/replace-act-import/tests/react-testing-library-style/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace-act-import-migrations": [ - { - "cardinality": { - "file": "tests/react-testing-library-style/input.tsx", - "pattern": "member-call" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-act-import/tests/real-world-test-file/expected.tsx b/replace-act-import/tests/real-world-test-file/expected.tsx deleted file mode 100644 index 9899718..0000000 --- a/replace-act-import/tests/real-world-test-file/expected.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; -import { UserProfile } from "./UserProfile"; - -describe("UserProfile", () => { - it("updates name when edited", async () => { - render(); - - const input = screen.getByRole("textbox", { name: /name/i }); - fireEvent.change(input, { target: { value: "Jane Doe" } }); - - await React.act(async () => { - fireEvent.click(screen.getByRole("button", { name: /save/i })); - }); - - expect(screen.getByText("Jane Doe")).toBeInTheDocument(); - }); - - it("shows loading state during save", async () => { - render(); - - await React.act(async () => { - fireEvent.click(screen.getByRole("button", { name: /save/i })); - }); - - expect(screen.getByText(/saving/i)).toHaveBeenVisible(); - }); -}); diff --git a/replace-act-import/tests/real-world-test-file/input.tsx b/replace-act-import/tests/real-world-test-file/input.tsx deleted file mode 100644 index e383c96..0000000 --- a/replace-act-import/tests/real-world-test-file/input.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from "react"; -import { render, screen, fireEvent } from "@testing-library/react"; -import ReactTestUtils from "react-dom/test-utils"; -import { UserProfile } from "./UserProfile"; - -describe("UserProfile", () => { - it("updates name when edited", async () => { - render(); - - const input = screen.getByRole("textbox", { name: /name/i }); - fireEvent.change(input, { target: { value: "Jane Doe" } }); - - await ReactTestUtils.act(async () => { - fireEvent.click(screen.getByRole("button", { name: /save/i })); - }); - - expect(screen.getByText("Jane Doe")).toBeInTheDocument(); - }); - - it("shows loading state during save", async () => { - render(); - - await ReactTestUtils.act(async () => { - fireEvent.click(screen.getByRole("button", { name: /save/i })); - }); - - expect(screen.getByText(/saving/i)).toHaveBeenVisible(); - }); -}); diff --git a/replace-act-import/tests/real-world-test-file/metrics.json b/replace-act-import/tests/real-world-test-file/metrics.json deleted file mode 100644 index 566bf4d..0000000 --- a/replace-act-import/tests/real-world-test-file/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace-act-import-migrations": [ - { - "cardinality": { - "file": "tests/real-world-test-file/input.tsx", - "pattern": "member-call" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/replace-act-import/tests/wildcard-import-2/expected.tsx b/replace-act-import/tests/wildcard-import-2/expected.tsx deleted file mode 100644 index 75678ea..0000000 --- a/replace-act-import/tests/wildcard-import-2/expected.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import * as React from "react"; -import * as ReactTestUtils from "react-dom/test-utils"; - -React.act(); diff --git a/replace-act-import/tsconfig.json b/replace-act-import/tsconfig.json deleted file mode 100644 index 469fc5a..0000000 --- a/replace-act-import/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], - "allowImportingTsExtensions": true, - "noEmit": true, - "verbatimModuleSyntax": true, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": ["tests"] -} diff --git a/replace-reactdom-render/README.md b/replace-reactdom-render/README.md deleted file mode 100644 index cfcf5c3..0000000 --- a/replace-reactdom-render/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# @react-codemods/replace-reactdom-render - -Replace ReactDOM.render with createRoot for React 18 - -## Installation - -```bash -# Install from registry -codemod run @react-codemods/replace-reactdom-render - -# Or run locally -codemod run -w workflow.yaml -``` - -## Usage - -This codemod transforms tsx code by: - -- Converting `var` declarations to `const`/`let` -- Removing debug statements -- Modernizing syntax patterns - -## Development - -```bash -# Test the transformation -npm test - -# Validate the workflow -codemod validate -w workflow.yaml - -# Publish to registry -codemod login -codemod publish -``` - -## License -MIT diff --git a/replace-reactdom-render/tests/multiple-render-calls/expected.tsx b/replace-reactdom-render/tests/multiple-render-calls/expected.tsx deleted file mode 100644 index 427d49a..0000000 --- a/replace-reactdom-render/tests/multiple-render-calls/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import ReactDOM from "react-dom"; -import App from "./App"; -import AdminApp from "./AdminApp";import { createRoot } from 'react-dom/client'; - - -const root = createRoot(document.getElementById("root")); -root.render(); -const root1 = createRoot(document.getElementById("admin-root")); -root1.render(); diff --git a/replace-reactdom-render/tests/multiple-render-calls/input.tsx b/replace-reactdom-render/tests/multiple-render-calls/input.tsx deleted file mode 100644 index 5c0ad19..0000000 --- a/replace-reactdom-render/tests/multiple-render-calls/input.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import ReactDOM from "react-dom"; -import App from "./App"; -import AdminApp from "./AdminApp"; - -ReactDOM.render(, document.getElementById("root")); -ReactDOM.render(, document.getElementById("admin-root")); diff --git a/replace-reactdom-render/tests/multiple-render-calls/metrics.json b/replace-reactdom-render/tests/multiple-render-calls/metrics.json deleted file mode 100644 index 3a36166..0000000 --- a/replace-reactdom-render/tests/multiple-render-calls/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "reactdom-render-replacements": [ - { - "cardinality": { - "file": "tests/multiple-render-calls/input.tsx", - "pattern": "ReactDOM.render" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/replace-reactdom-render/tests/named-import/expected.tsx b/replace-reactdom-render/tests/named-import/expected.tsx deleted file mode 100644 index c1fe685..0000000 --- a/replace-reactdom-render/tests/named-import/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { render } from "react-dom"; -import App from "./App";import { createRoot } from 'react-dom/client'; - - -const root = createRoot(document.getElementById("root")); -root.render(); diff --git a/replace-reactdom-render/tests/named-import/input.tsx b/replace-reactdom-render/tests/named-import/input.tsx deleted file mode 100644 index 6c1cee3..0000000 --- a/replace-reactdom-render/tests/named-import/input.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { render } from "react-dom"; -import App from "./App"; - -render(, document.getElementById("root")); diff --git a/replace-reactdom-render/tests/no-reactdom-import/expected.tsx b/replace-reactdom-render/tests/no-reactdom-import/expected.tsx deleted file mode 100644 index d188567..0000000 --- a/replace-reactdom-render/tests/no-reactdom-import/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import App from "./App"; - -// Already using createRoot - no react-dom render -const root = document.getElementById("root"); -if (root) { - console.log("Mount app to root"); -} - -export {}; diff --git a/replace-reactdom-render/tests/no-reactdom-import/input.tsx b/replace-reactdom-render/tests/no-reactdom-import/input.tsx deleted file mode 100644 index d188567..0000000 --- a/replace-reactdom-render/tests/no-reactdom-import/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import App from "./App"; - -// Already using createRoot - no react-dom render -const root = document.getElementById("root"); -if (root) { - console.log("Mount app to root"); -} - -export {}; diff --git a/replace-reactdom-render/tests/original-default/metrics.json b/replace-reactdom-render/tests/original-default/metrics.json deleted file mode 100644 index 77ef1b0..0000000 --- a/replace-reactdom-render/tests/original-default/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "reactdom-render-replacements": [ - { - "cardinality": { - "file": "tests/original-default/input.tsx", - "pattern": "ReactDOM.render" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-reactdom-render/tests/original-nested/expected.tsx b/replace-reactdom-render/tests/original-nested/expected.tsx deleted file mode 100644 index fc06116..0000000 --- a/replace-reactdom-render/tests/original-nested/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { render } from "react-dom";import { createRoot } from 'react-dom/client'; - - -const fn = () => { - if (true) { - const root = createRoot(theNode); - root.render(); - } -}; diff --git a/replace-reactdom-render/tests/original-nested/metrics.json b/replace-reactdom-render/tests/original-nested/metrics.json deleted file mode 100644 index 44a8e83..0000000 --- a/replace-reactdom-render/tests/original-nested/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "reactdom-render-replacements": [ - { - "cardinality": { - "file": "tests/original-nested/input.tsx", - "pattern": "render" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-reactdom-render/tests/reactdom-member/expected.tsx b/replace-reactdom-render/tests/reactdom-member/expected.tsx deleted file mode 100644 index e02df57..0000000 --- a/replace-reactdom-render/tests/reactdom-member/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import ReactDOM from "react-dom"; -import App from "./App";import { createRoot } from 'react-dom/client'; - - -const root = createRoot(document.getElementById("root")); -root.render(); diff --git a/replace-reactdom-render/tests/reactdom-member/input.tsx b/replace-reactdom-render/tests/reactdom-member/input.tsx deleted file mode 100644 index ce92b1c..0000000 --- a/replace-reactdom-render/tests/reactdom-member/input.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import ReactDOM from "react-dom"; -import App from "./App"; - -ReactDOM.render(, document.getElementById("root")); diff --git a/replace-reactdom-render/tsconfig.json b/replace-reactdom-render/tsconfig.json deleted file mode 100644 index 469fc5a..0000000 --- a/replace-reactdom-render/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], - "allowImportingTsExtensions": true, - "noEmit": true, - "verbatimModuleSyntax": true, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": ["tests"] -} diff --git a/replace-string-ref/README.md b/replace-string-ref/README.md deleted file mode 100644 index 92f9593..0000000 --- a/replace-string-ref/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# @react-codemods/replace-string-ref - -Replace string refs with callback refs in React class components - -## Installation - -```bash -# Install from registry -codemod run @react-codemods/replace-string-ref - -# Or run locally -codemod run -w workflow.yaml -``` - -## Usage - -This codemod transforms tsx code by: - -- Converting `var` declarations to `const`/`let` -- Removing debug statements -- Modernizing syntax patterns - -## Development - -```bash -# Test the transformation -npm test - -# Validate the workflow -codemod validate -w workflow.yaml - -# Publish to registry -codemod login -codemod publish -``` - -## License -MIT diff --git a/replace-string-ref/tests/basic-string-ref/expected.tsx b/replace-string-ref/tests/basic-string-ref/expected.tsx deleted file mode 100644 index 73aef79..0000000 --- a/replace-string-ref/tests/basic-string-ref/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - render() { - return { - this.refs.myInput = ref; - }} />; - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/basic-string-ref/input.tsx b/replace-string-ref/tests/basic-string-ref/input.tsx deleted file mode 100644 index ad73be3..0000000 --- a/replace-string-ref/tests/basic-string-ref/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - render() { - return ; - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/basic-string-ref/metrics.json b/replace-string-ref/tests/basic-string-ref/metrics.json deleted file mode 100644 index 82c3ed9..0000000 --- a/replace-string-ref/tests/basic-string-ref/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "string-ref-replacements": [ - { - "cardinality": { - "file": "tests/basic-string-ref/input.tsx", - "refs": "myInput" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-string-ref/tests/callback-ref-no-op/expected.tsx b/replace-string-ref/tests/callback-ref-no-op/expected.tsx deleted file mode 100644 index f6a4118..0000000 --- a/replace-string-ref/tests/callback-ref-no-op/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - inputRef = React.createRef(); - - render() { - return ; - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/callback-ref-no-op/input.tsx b/replace-string-ref/tests/callback-ref-no-op/input.tsx deleted file mode 100644 index f6a4118..0000000 --- a/replace-string-ref/tests/callback-ref-no-op/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - inputRef = React.createRef(); - - render() { - return ; - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/expression-ref-no-op/expected.tsx b/replace-string-ref/tests/expression-ref-no-op/expected.tsx deleted file mode 100644 index 1a52b25..0000000 --- a/replace-string-ref/tests/expression-ref-no-op/expected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - render() { - const refName = "myInput"; - return ; - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/expression-ref-no-op/input.tsx b/replace-string-ref/tests/expression-ref-no-op/input.tsx deleted file mode 100644 index 1a52b25..0000000 --- a/replace-string-ref/tests/expression-ref-no-op/input.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - render() { - const refName = "myInput"; - return ; - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/multiple-refs/expected.tsx b/replace-string-ref/tests/multiple-refs/expected.tsx deleted file mode 100644 index 0e27c65..0000000 --- a/replace-string-ref/tests/multiple-refs/expected.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from "react"; - -class MyComponent extends React.Component { - render() { - return ( -
    - { - this.refs.input1 = ref; - }} /> - { - this.refs.input2 = ref; - }} /> - -
    - ); - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/multiple-refs/input.tsx b/replace-string-ref/tests/multiple-refs/input.tsx deleted file mode 100644 index 7a7c605..0000000 --- a/replace-string-ref/tests/multiple-refs/input.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from "react"; - -class MyComponent extends React.Component { - render() { - return ( -
    - - - -
    - ); - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/multiple-refs/metrics.json b/replace-string-ref/tests/multiple-refs/metrics.json deleted file mode 100644 index 7b2b9a6..0000000 --- a/replace-string-ref/tests/multiple-refs/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "string-ref-replacements": [ - { - "cardinality": { - "file": "tests/multiple-refs/input.tsx", - "refs": "input1,input2,submitBtn" - }, - "count": 3 - } - ] -} \ No newline at end of file diff --git a/replace-string-ref/tests/single-ref-in-fragment/expected.tsx b/replace-string-ref/tests/single-ref-in-fragment/expected.tsx deleted file mode 100644 index be414e2..0000000 --- a/replace-string-ref/tests/single-ref-in-fragment/expected.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - render() { - return ( - <> -
    Header
    - { - this.refs.searchInput = ref; - }} /> - - ); - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/single-ref-in-fragment/input.tsx b/replace-string-ref/tests/single-ref-in-fragment/input.tsx deleted file mode 100644 index cdbc365..0000000 --- a/replace-string-ref/tests/single-ref-in-fragment/input.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React, { Component } from "react"; - -class MyComponent extends Component { - render() { - return ( - <> -
    Header
    - - - ); - } -} - -export default MyComponent; diff --git a/replace-string-ref/tests/single-ref-in-fragment/metrics.json b/replace-string-ref/tests/single-ref-in-fragment/metrics.json deleted file mode 100644 index e2b2088..0000000 --- a/replace-string-ref/tests/single-ref-in-fragment/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "string-ref-replacements": [ - { - "cardinality": { - "file": "tests/single-ref-in-fragment/input.tsx", - "refs": "searchInput" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-string-ref/tsconfig.json b/replace-string-ref/tsconfig.json deleted file mode 100644 index 469fc5a..0000000 --- a/replace-string-ref/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], - "allowImportingTsExtensions": true, - "noEmit": true, - "verbatimModuleSyntax": true, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": ["tests"] -} diff --git a/replace-use-form-state/tests/different-package-no-op/expected.tsx b/replace-use-form-state/tests/different-package-no-op/expected.tsx deleted file mode 100644 index 80c66fb..0000000 --- a/replace-use-form-state/tests/different-package-no-op/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { useFormState } from "fake-form-library"; - -function Form() { - const form = useFormState({ initial: {} }); - return
    {form.fields}; -} diff --git a/replace-use-form-state/tests/different-package-no-op/input.tsx b/replace-use-form-state/tests/different-package-no-op/input.tsx deleted file mode 100644 index 80c66fb..0000000 --- a/replace-use-form-state/tests/different-package-no-op/input.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { useFormState } from "fake-form-library"; - -function Form() { - const form = useFormState({ initial: {} }); - return
    {form.fields}; -} diff --git a/replace-use-form-state/tests/local-variable-shadow-no-op/expected.tsx b/replace-use-form-state/tests/local-variable-shadow-no-op/expected.tsx deleted file mode 100644 index 77ba700..0000000 --- a/replace-use-form-state/tests/local-variable-shadow-no-op/expected.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useActionState } from "react-dom"; - -function Form() { - const [state, formAction] = useActionState(submit, null); - - function handleClick() { - const useFormState = "local variable - should NOT be replaced"; - console.log(useFormState); - } - - return ( -
    - - - - ); -} - -async function submit(_prev: string | null, _data: FormData) { - return "ok"; -} diff --git a/replace-use-form-state/tests/local-variable-shadow-no-op/input.tsx b/replace-use-form-state/tests/local-variable-shadow-no-op/input.tsx deleted file mode 100644 index 689d853..0000000 --- a/replace-use-form-state/tests/local-variable-shadow-no-op/input.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useFormState } from "react-dom"; - -function Form() { - const [state, formAction] = useFormState(submit, null); - - function handleClick() { - const useFormState = "local variable - should NOT be replaced"; - console.log(useFormState); - } - - return ( -
    - - - - ); -} - -async function submit(_prev: string | null, _data: FormData) { - return "ok"; -} diff --git a/replace-use-form-state/tests/local-variable-shadow-no-op/metrics.json b/replace-use-form-state/tests/local-variable-shadow-no-op/metrics.json deleted file mode 100644 index d504896..0000000 --- a/replace-use-form-state/tests/local-variable-shadow-no-op/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace-use-form-state-migrations": [ - { - "cardinality": { - "file": "tests/local-variable-shadow-no-op/input.tsx", - "pattern": "named-import" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-use-form-state/tests/multiple-usages/expected.tsx b/replace-use-form-state/tests/multiple-usages/expected.tsx deleted file mode 100644 index 9e760fb..0000000 --- a/replace-use-form-state/tests/multiple-usages/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useActionState } from "react-dom"; - -function Form1() { - const [a, setA] = useActionState(fn, null); - return
    {a}; -} - -function Form2() { - const [b, setB] = useActionState(fn, null); - return
    {b}; -} diff --git a/replace-use-form-state/tests/multiple-usages/input.tsx b/replace-use-form-state/tests/multiple-usages/input.tsx deleted file mode 100644 index 0823603..0000000 --- a/replace-use-form-state/tests/multiple-usages/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useFormState } from "react-dom"; - -function Form1() { - const [a, setA] = useFormState(fn, null); - return
    {a}; -} - -function Form2() { - const [b, setB] = useFormState(fn, null); - return
    {b}; -} diff --git a/replace-use-form-state/tests/multiple-usages/metrics.json b/replace-use-form-state/tests/multiple-usages/metrics.json deleted file mode 100644 index b5c1c62..0000000 --- a/replace-use-form-state/tests/multiple-usages/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace-use-form-state-migrations": [ - { - "cardinality": { - "file": "tests/multiple-usages/input.tsx", - "pattern": "named-import" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-use-form-state/tests/real-world-form-component/expected.tsx b/replace-use-form-state/tests/real-world-form-component/expected.tsx deleted file mode 100644 index 96ace59..0000000 --- a/replace-use-form-state/tests/real-world-form-component/expected.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import { useActionState } from "react-dom"; -import { useFormStatus } from "react-dom"; - -async function createPost(_prev: { error?: string } | null, formData: FormData) { - const title = formData.get("title") as string; - if (!title?.trim()) { - return { error: "Title is required" }; - } - await fetch("/api/posts", { - method: "POST", - body: JSON.stringify({ title }), - }); - return { error: undefined }; -} - -function SubmitButton() { - const { pending } = useFormStatus(); - return ( - - ); -} - -export function CreatePostForm() { - const [state, formAction] = useActionState(createPost, null); - - return ( -
    -
    - - -
    - {state?.error &&

    {state.error}

    } - - - ); -} diff --git a/replace-use-form-state/tests/real-world-form-component/input.tsx b/replace-use-form-state/tests/real-world-form-component/input.tsx deleted file mode 100644 index 40a4832..0000000 --- a/replace-use-form-state/tests/real-world-form-component/input.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import { useFormState } from "react-dom"; -import { useFormStatus } from "react-dom"; - -async function createPost(_prev: { error?: string } | null, formData: FormData) { - const title = formData.get("title") as string; - if (!title?.trim()) { - return { error: "Title is required" }; - } - await fetch("/api/posts", { - method: "POST", - body: JSON.stringify({ title }), - }); - return { error: undefined }; -} - -function SubmitButton() { - const { pending } = useFormStatus(); - return ( - - ); -} - -export function CreatePostForm() { - const [state, formAction] = useFormState(createPost, null); - - return ( -
    -
    - - -
    - {state?.error &&

    {state.error}

    } - - - ); -} diff --git a/replace-use-form-state/tests/real-world-form-component/metrics.json b/replace-use-form-state/tests/real-world-form-component/metrics.json deleted file mode 100644 index 2b872d4..0000000 --- a/replace-use-form-state/tests/real-world-form-component/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "replace-use-form-state-migrations": [ - { - "cardinality": { - "file": "tests/real-world-form-component/input.tsx", - "pattern": "named-import" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/replace-use-form-state/tsconfig.json b/replace-use-form-state/tsconfig.json deleted file mode 100644 index 469fc5a..0000000 --- a/replace-use-form-state/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], - "allowImportingTsExtensions": true, - "noEmit": true, - "verbatimModuleSyntax": true, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": ["tests"] -} diff --git a/sort-comp/scripts/codemod.ts b/sort-comp/scripts/codemod.ts deleted file mode 100644 index 96aafc2..0000000 --- a/sort-comp/scripts/codemod.ts +++ /dev/null @@ -1,139 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -const DEFAULT_METHODS_ORDER = [ - "static-methods", - "displayName", - "propTypes", - "contextTypes", - "childContextTypes", - "mixins", - "statics", - "defaultProps", - "constructor", - "getDefaultProps", - "state", - "getInitialState", - "getChildContext", - "getDerivedStateFromProps", - "componentWillMount", - "UNSAFE_componentWillMount", - "componentDidMount", - "componentWillReceiveProps", - "UNSAFE_componentWillReceiveProps", - "shouldComponentUpdate", - "componentWillUpdate", - "UNSAFE_componentWillUpdate", - "getSnapshotBeforeUpdate", - "componentDidUpdate", - "componentDidCatch", - "componentWillUnmount", - "/^on.+$/", - "/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/", - "everything-else", - "/^render.+$/", - "render", -]; - -const REGEXP_REGEXP = /^\/(.*)\/([gimuy]*)$/; - -function selectorMatches(selector: string, methodName: string): boolean { - if (selector === methodName) return true; - if (selector === "static-methods") return false; - const reMatch = selector.match(REGEXP_REGEXP); - if (reMatch) { - try { - const re = new RegExp(reMatch[1], reMatch[2]); - return re.test(methodName); - } catch { - return false; - } - } - return false; -} - -function getCorrectIndex(methodsOrder: string[], methodName: string): number { - const everythingElseIndex = methodsOrder.indexOf("everything-else"); - for (let i = 0; i < methodsOrder.length; i++) { - if (i !== everythingElseIndex && selectorMatches(methodsOrder[i], methodName)) { - return i; - } - } - return everythingElseIndex >= 0 ? everythingElseIndex : Number.POSITIVE_INFINITY; -} - -function getPairKeyName(node: SgNode): string { - if (node.kind() === "method_definition") { - const name = node.field("name"); - return name ? name.text() : ""; - } - const key = node.field("key"); - if (!key) return ""; - return key.text(); -} - -const transform: Transform = async (root, options) => { - const rootNode = root.root(); - const source = rootNode.text(); - const edits: Edit[] = []; - const methodsOrder = (options.params?.["methodsOrder"] as string[]) ?? DEFAULT_METHODS_ORDER; - const metric = useMetricAtom("sort-comp-reorders"); - - const createClassCalls = rootNode.findAll({ - rule: { - kind: "call_expression", - has: { - field: "function", - any: [ - { - kind: "member_expression", - all: [ - { has: { field: "object", kind: "identifier", regex: "^React$" } }, - { has: { field: "property", kind: "property_identifier", regex: "^createClass$" } }, - ], - }, - { kind: "identifier", regex: "^createClass$" }, - ], - }, - }, - }); - - for (const call of createClassCalls) { - const args = call.field("arguments"); - if (!args) continue; - const argList = args.children().filter( - (c) => c.kind() !== "(" && c.kind() !== ")" && c.kind() !== ",", - ); - const configObj = argList[0]; - if (!configObj || configObj.kind() !== "object") continue; - - const pairs = configObj.children().filter( - (c) => c.kind() === "pair" || c.kind() === "method_definition", - ); - if (pairs.length <= 1) continue; - - const sorted = [...pairs].sort((a, b) => { - const nameA = getPairKeyName(a); - const nameB = getPairKeyName(b); - const idxA = getCorrectIndex(methodsOrder, nameA); - const idxB = getCorrectIndex(methodsOrder, nameB); - if (idxA !== idxB) return idxA - idxB; - return nameA.localeCompare(nameB); - }); - - const first = pairs[0]!; - const last = pairs[pairs.length - 1]!; - const start = first.range().start.index; - const end = last.range().end.index; - const sep = ",\n\n "; - const newContent = sorted.map((p) => p.text()).join(sep); - edits.push({ startPos: start, endPos: end, insertedText: newContent }); - metric.increment({ file: root.filename() }); - } - - if (edits.length === 0) return null; - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/sort-comp/tests/lifecycle-order/expected.tsx b/sort-comp/tests/lifecycle-order/expected.tsx deleted file mode 100644 index a11c5c4..0000000 --- a/sort-comp/tests/lifecycle-order/expected.tsx +++ /dev/null @@ -1,25 +0,0 @@ -var React = require('react'); - -var LifecycleDemo = React.createClass({ - getInitialState: function() { - return {}; - }, - - componentWillMount: function() { - console.log('will mount'); - }, - - componentDidMount: function() { - console.log('mounted'); - }, - - componentWillUnmount: function() { - console.log('unmounting'); - }, - - render: function() { - return
    ; - }, -}); - -module.exports = LifecycleDemo; diff --git a/sort-comp/tests/lifecycle-order/input.tsx b/sort-comp/tests/lifecycle-order/input.tsx deleted file mode 100644 index 3ac938d..0000000 --- a/sort-comp/tests/lifecycle-order/input.tsx +++ /dev/null @@ -1,25 +0,0 @@ -var React = require('react'); - -var LifecycleDemo = React.createClass({ - render: function() { - return
    ; - }, - - componentDidMount: function() { - console.log('mounted'); - }, - - componentWillUnmount: function() { - console.log('unmounting'); - }, - - componentWillMount: function() { - console.log('will mount'); - }, - - getInitialState: function() { - return {}; - }, -}); - -module.exports = LifecycleDemo; diff --git a/sort-comp/tests/no-reorder-needed/expected.tsx b/sort-comp/tests/no-reorder-needed/expected.tsx deleted file mode 100644 index 78a5e48..0000000 --- a/sort-comp/tests/no-reorder-needed/expected.tsx +++ /dev/null @@ -1,13 +0,0 @@ -var React = require('react'); - -var C = React.createClass({ - displayName: 'C', - - propTypes: {}, - - render: function() { - return
    ; - }, -}); - -module.exports = C; diff --git a/sort-comp/tests/no-reorder-needed/input.tsx b/sort-comp/tests/no-reorder-needed/input.tsx deleted file mode 100644 index a321e98..0000000 --- a/sort-comp/tests/no-reorder-needed/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -var React = require('react'); - -var C = React.createClass({ - displayName: 'C', - propTypes: {}, - render: function() { - return
    ; - }, -}); - -module.exports = C; diff --git a/sort-comp/tests/real-world-container/expected.tsx b/sort-comp/tests/real-world-container/expected.tsx deleted file mode 100644 index 3222c4b..0000000 --- a/sort-comp/tests/real-world-container/expected.tsx +++ /dev/null @@ -1,37 +0,0 @@ -var React = require('react'); - -var DataContainer = React.createClass({ - propTypes: { - url: React.PropTypes.string.isRequired, - children: React.PropTypes.node, - }, - - defaultProps: { - pollInterval: 5000, - }, - - getInitialState: function() { - return { loading: true }; - }, - - componentDidMount: function() { - this.fetchData(); - }, - - fetchData: function() { - var self = this; - fetch(this.props.url).then(function(r) { return r.json(); }).then(function(data) { - self.setState({ loading: false, data: data }); - }); - }, - - render: function() { - return ( -
    - {this.state.loading ? Loading... : this.props.children} -
    - ); - }, -}); - -module.exports = DataContainer; diff --git a/sort-comp/tests/real-world-container/input.tsx b/sort-comp/tests/real-world-container/input.tsx deleted file mode 100644 index f1e6eb3..0000000 --- a/sort-comp/tests/real-world-container/input.tsx +++ /dev/null @@ -1,37 +0,0 @@ -var React = require('react'); - -var DataContainer = React.createClass({ - render: function() { - return ( -
    - {this.state.loading ? Loading... : this.props.children} -
    - ); - }, - - getInitialState: function() { - return { loading: true }; - }, - - defaultProps: { - pollInterval: 5000, - }, - - componentDidMount: function() { - this.fetchData(); - }, - - fetchData: function() { - var self = this; - fetch(this.props.url).then(function(r) { return r.json(); }).then(function(data) { - self.setState({ loading: false, data: data }); - }); - }, - - propTypes: { - url: React.PropTypes.string.isRequired, - children: React.PropTypes.node, - }, -}); - -module.exports = DataContainer; diff --git a/sort-comp/tests/real-world-search-input/expected.tsx b/sort-comp/tests/real-world-search-input/expected.tsx deleted file mode 100644 index 10cfe33..0000000 --- a/sort-comp/tests/real-world-search-input/expected.tsx +++ /dev/null @@ -1,42 +0,0 @@ -var React = require('react'); - -var SearchInput = React.createClass({ - propTypes: { - placeholder: React.PropTypes.string, - onSearch: React.PropTypes.func, - }, - - getInitialState: function() { - return { value: '' }; - }, - - componentDidMount: function() { - this.inputRef.focus(); - }, - - handleChange: function(e) { - this.setState({ value: e.target.value }); - }, - - handleClear: function() { - this.setState({ value: '' }); - }, - - render: function() { - return ( -
    - { this.inputRef = el; }} - value={this.state.value} - onChange={this.handleChange} - placeholder={this.props.placeholder} - /> - {this.state.value && ( - - )} -
    - ); - }, -}); - -module.exports = SearchInput; diff --git a/sort-comp/tests/real-world-search-input/input.tsx b/sort-comp/tests/real-world-search-input/input.tsx deleted file mode 100644 index 9add27a..0000000 --- a/sort-comp/tests/real-world-search-input/input.tsx +++ /dev/null @@ -1,42 +0,0 @@ -var React = require('react'); - -var SearchInput = React.createClass({ - handleClear: function() { - this.setState({ value: '' }); - }, - - handleChange: function(e) { - this.setState({ value: e.target.value }); - }, - - getInitialState: function() { - return { value: '' }; - }, - - componentDidMount: function() { - this.inputRef.focus(); - }, - - propTypes: { - placeholder: React.PropTypes.string, - onSearch: React.PropTypes.func, - }, - - render: function() { - return ( -
    - { this.inputRef = el; }} - value={this.state.value} - onChange={this.handleChange} - placeholder={this.props.placeholder} - /> - {this.state.value && ( - - )} -
    - ); - }, -}); - -module.exports = SearchInput; diff --git a/sort-comp/tests/real-world-search-input/metrics.json b/sort-comp/tests/real-world-search-input/metrics.json deleted file mode 100644 index 410d069..0000000 --- a/sort-comp/tests/real-world-search-input/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/real-world-search-input/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/sort-comp/tests/real-world-settings-panel/expected.tsx b/sort-comp/tests/real-world-settings-panel/expected.tsx deleted file mode 100644 index 80aa311..0000000 --- a/sort-comp/tests/real-world-settings-panel/expected.tsx +++ /dev/null @@ -1,26 +0,0 @@ -var React = require('react'); - -var SettingsPanel = React.createClass({ - propTypes: { - children: React.PropTypes.node, - }, - - getInitialState: function() { - return { expanded: false }; - }, - - toggle: function() { - this.setState({ expanded: !this.state.expanded }); - }, - - render: function() { - return ( -
    - - {this.state.expanded &&
    {this.props.children}
    } -
    - ); - }, -}); - -module.exports = SettingsPanel; diff --git a/sort-comp/tests/real-world-settings-panel/input.tsx b/sort-comp/tests/real-world-settings-panel/input.tsx deleted file mode 100644 index fb5e5e5..0000000 --- a/sort-comp/tests/real-world-settings-panel/input.tsx +++ /dev/null @@ -1,26 +0,0 @@ -var React = require('react'); - -var SettingsPanel = React.createClass({ - getInitialState: function() { - return { expanded: false }; - }, - - toggle: function() { - this.setState({ expanded: !this.state.expanded }); - }, - - render: function() { - return ( -
    - - {this.state.expanded &&
    {this.props.children}
    } -
    - ); - }, - - propTypes: { - children: React.PropTypes.node, - }, -}); - -module.exports = SettingsPanel; diff --git a/sort-comp/tests/real-world-settings-panel/metrics.json b/sort-comp/tests/real-world-settings-panel/metrics.json deleted file mode 100644 index 96ea420..0000000 --- a/sort-comp/tests/real-world-settings-panel/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/real-world-settings-panel/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/sort-comp/tests/real-world-tabbed-panel/expected.tsx b/sort-comp/tests/real-world-tabbed-panel/expected.tsx deleted file mode 100644 index 3eebd22..0000000 --- a/sort-comp/tests/real-world-tabbed-panel/expected.tsx +++ /dev/null @@ -1,29 +0,0 @@ -var React = require('react'); - -var TabbedPanel = React.createClass({ - propTypes: { - tabs: React.PropTypes.array.isRequired, - }, - - statics: { - defaultActiveIndex: 0, - }, - - getInitialState: function() { - return { activeTab: null }; - }, - - getActiveContent: function() { - return this.props.tabs.find(function(t) { return t.id === this.state.activeTab; }.bind(this)); - }, - - selectTab: function(tabId) { - this.setState({ activeTab: tabId }); - }, - - render: function() { - return
    {this.getActiveContent()}
    ; - }, -}); - -module.exports = TabbedPanel; diff --git a/sort-comp/tests/real-world-tabbed-panel/input.tsx b/sort-comp/tests/real-world-tabbed-panel/input.tsx deleted file mode 100644 index 2503103..0000000 --- a/sort-comp/tests/real-world-tabbed-panel/input.tsx +++ /dev/null @@ -1,29 +0,0 @@ -var React = require('react'); - -var TabbedPanel = React.createClass({ - selectTab: function(tabId) { - this.setState({ activeTab: tabId }); - }, - - getActiveContent: function() { - return this.props.tabs.find(function(t) { return t.id === this.state.activeTab; }.bind(this)); - }, - - getInitialState: function() { - return { activeTab: null }; - }, - - statics: { - defaultActiveIndex: 0, - }, - - propTypes: { - tabs: React.PropTypes.array.isRequired, - }, - - render: function() { - return
    {this.getActiveContent()}
    ; - }, -}); - -module.exports = TabbedPanel; diff --git a/sort-comp/tests/real-world-tabbed-panel/metrics.json b/sort-comp/tests/real-world-tabbed-panel/metrics.json deleted file mode 100644 index 163e5e9..0000000 --- a/sort-comp/tests/real-world-tabbed-panel/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/real-world-tabbed-panel/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/sort-comp/tests/render-helpers-order/expected.tsx b/sort-comp/tests/render-helpers-order/expected.tsx deleted file mode 100644 index 3ecb2eb..0000000 --- a/sort-comp/tests/render-helpers-order/expected.tsx +++ /dev/null @@ -1,26 +0,0 @@ -var React = require('react'); - -var CardList = React.createClass({ - propTypes: { - items: React.PropTypes.array, - }, - - renderCard: function(item) { - return
    {item.name}
    ; - }, - - renderHeader: function() { - return

    Header

    ; - }, - - render: function() { - return ( -
    - {this.renderHeader()} - {this.props.items.map(this.renderCard)} -
    - ); - }, -}); - -module.exports = CardList; diff --git a/sort-comp/tests/render-helpers-order/input.tsx b/sort-comp/tests/render-helpers-order/input.tsx deleted file mode 100644 index b0d4f8a..0000000 --- a/sort-comp/tests/render-helpers-order/input.tsx +++ /dev/null @@ -1,26 +0,0 @@ -var React = require('react'); - -var CardList = React.createClass({ - renderHeader: function() { - return

    Header

    ; - }, - - renderCard: function(item) { - return
    {item.name}
    ; - }, - - propTypes: { - items: React.PropTypes.array, - }, - - render: function() { - return ( -
    - {this.renderHeader()} - {this.props.items.map(this.renderCard)} -
    - ); - }, -}); - -module.exports = CardList; diff --git a/sort-comp/tests/render-helpers-order/metrics.json b/sort-comp/tests/render-helpers-order/metrics.json deleted file mode 100644 index 2867315..0000000 --- a/sort-comp/tests/render-helpers-order/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/render-helpers-order/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/update-react-imports/README.md b/update-react-imports/README.md deleted file mode 100644 index c10ade6..0000000 --- a/update-react-imports/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# @react-codemods/update-react-imports - -Remove unused React default import and convert to named imports - -## Installation - -```bash -# Install from registry -codemod run @react-codemods/update-react-imports - -# Or run locally -codemod run -w workflow.yaml -``` - -## Usage - -This codemod transforms tsx code by: - -- Converting `var` declarations to `const`/`let` -- Removing debug statements -- Modernizing syntax patterns - -## Development - -```bash -# Test the transformation -npm test - -# Validate the workflow -codemod validate -w workflow.yaml - -# Publish to registry -codemod login -codemod publish -``` - -## License -MIT diff --git a/update-react-imports/scripts/codemod.ts b/update-react-imports/scripts/codemod.ts deleted file mode 100644 index 11ec5cc..0000000 --- a/update-react-imports/scripts/codemod.ts +++ /dev/null @@ -1,143 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; -import { getImport, removeImport } from "@jssg/utils/javascript/imports"; - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const edits: Edit[] = []; - - const transformMetric = useMetricAtom("react-import-updates"); - - const reactDefaultImport = getImport(rootNode, { type: "default", from: "react" }); - - if (!reactDefaultImport) { - return null; - } - - const reactName = reactDefaultImport.alias; - - const reactMemberUsages = new Map[]>(); - - const memberExpressions = rootNode.findAll({ - rule: { - kind: "member_expression", - has: { - field: "object", - kind: "identifier", - regex: `^${reactName}$`, - }, - }, - }); - - for (const memberExpr of memberExpressions) { - const property = memberExpr.find({ - rule: { - kind: "property_identifier", - }, - }); - - if (property) { - const propName = property.text(); - if (!reactMemberUsages.has(propName)) { - reactMemberUsages.set(propName, []); - } - reactMemberUsages.get(propName)!.push(memberExpr); - } - } - - - const reactAsValue = rootNode.findAll({ - rule: { - kind: "identifier", - regex: `^${reactName}$`, - }, - }).filter((node) => { - const parent = node.parent(); - if (!parent) return true; - - if (parent.kind() === "member_expression") { - const objectField = parent.field("object"); - if (objectField && objectField.id() === node.id()) { - return false; - } - } - - const ancestors = node.ancestors(); - for (const ancestor of ancestors) { - if (ancestor.kind() === "import_statement") { - return false; - } - } - - return true; - }); - - const isReactUsedAsValue = reactAsValue.length > 0; - - if (isReactUsedAsValue) { - const newImportText = `import * as ${reactName} from "react";`; - const importStatement = reactDefaultImport.node.ancestors().find( - (a) => a.kind() === "import_statement" - ); - if (importStatement) { - edits.push(importStatement.replace(newImportText)); - } - - transformMetric.increment({ - action: "convert-to-namespace", - reason: "react-used-as-value", - file: root.filename(), - }); - } else if (reactMemberUsages.size > 0) { - const namedImportsToAdd: string[] = []; - let totalConversions = 0; - - for (const [memberName, usages] of reactMemberUsages) { - namedImportsToAdd.push(memberName); - - for (const usage of usages) { - edits.push(usage.replace(memberName)); - } - - totalConversions += usages.length; - } - - const namedList = [...new Set(namedImportsToAdd)].sort().join(", "); - const newImportText = `import { ${namedList} } from "react";`; - const importStatement = reactDefaultImport.node.ancestors().find( - (a) => a.kind() === "import_statement" - ); - if (importStatement) { - edits.push(importStatement.replace(newImportText)); - } - - transformMetric.increment({ - action: "convert-member-to-named", - members: namedList, - file: root.filename(), - }, totalConversions); - } else { - - const removeEdit = removeImport(rootNode, { - type: "default", - from: "react", - }); - if (removeEdit) { - edits.push(removeEdit); - transformMetric.increment({ - action: "remove-default-import", - reason: "only-jsx-usage", - file: root.filename(), - }); - } - } - - if (edits.length === 0) { - return null; - } - - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/update-react-imports/tests/fragment-only/expected.tsx b/update-react-imports/tests/fragment-only/expected.tsx deleted file mode 100644 index b826e27..0000000 --- a/update-react-imports/tests/fragment-only/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ - -function MyComponent() { - return ( - <> -
    Hello
    - World - - ); -} - -export default MyComponent; diff --git a/update-react-imports/tests/fragment-only/input.tsx b/update-react-imports/tests/fragment-only/input.tsx deleted file mode 100644 index 4c40584..0000000 --- a/update-react-imports/tests/fragment-only/input.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react"; - -function MyComponent() { - return ( - <> -
    Hello
    - World - - ); -} - -export default MyComponent; diff --git a/update-react-imports/tests/fragment-only/metrics.json b/update-react-imports/tests/fragment-only/metrics.json deleted file mode 100644 index 4e76348..0000000 --- a/update-react-imports/tests/fragment-only/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-default-import", - "file": "tests/fragment-only/input.tsx", - "reason": "only-jsx-usage" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/update-react-imports/tests/jsx-only/expected.tsx b/update-react-imports/tests/jsx-only/expected.tsx deleted file mode 100644 index c45e4ae..0000000 --- a/update-react-imports/tests/jsx-only/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ - -function MyComponent() { - return
    Hello World
    ; -} - -export default MyComponent; diff --git a/update-react-imports/tests/jsx-only/input.tsx b/update-react-imports/tests/jsx-only/input.tsx deleted file mode 100644 index 0a80c61..0000000 --- a/update-react-imports/tests/jsx-only/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; - -function MyComponent() { - return
    Hello World
    ; -} - -export default MyComponent; diff --git a/update-react-imports/tests/jsx-only/metrics.json b/update-react-imports/tests/jsx-only/metrics.json deleted file mode 100644 index 99d10c6..0000000 --- a/update-react-imports/tests/jsx-only/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-default-import", - "file": "tests/jsx-only/input.tsx", - "reason": "only-jsx-usage" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/update-react-imports/tests/mixed-usage/expected.tsx b/update-react-imports/tests/mixed-usage/expected.tsx deleted file mode 100644 index b0e0ac9..0000000 --- a/update-react-imports/tests/mixed-usage/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Component, createElement } from "react"; - -class MyComponent extends Component { - render() { - return createElement('div', {}, 'Hello'); - } -} - -export default MyComponent; diff --git a/update-react-imports/tests/mixed-usage/input.tsx b/update-react-imports/tests/mixed-usage/input.tsx deleted file mode 100644 index 5b80cf6..0000000 --- a/update-react-imports/tests/mixed-usage/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; - -class MyComponent extends React.Component { - render() { - return React.createElement('div', {}, 'Hello'); - } -} - -export default MyComponent; diff --git a/update-react-imports/tests/mixed-usage/metrics.json b/update-react-imports/tests/mixed-usage/metrics.json deleted file mode 100644 index 2dcbdb5..0000000 --- a/update-react-imports/tests/mixed-usage/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-member-to-named", - "file": "tests/mixed-usage/input.tsx", - "members": "Component, createElement" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/update-react-imports/tests/no-react-import/expected.tsx b/update-react-imports/tests/no-react-import/expected.tsx deleted file mode 100644 index a8baecb..0000000 --- a/update-react-imports/tests/no-react-import/expected.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useState } from "react"; - -function MyComponent() { - const [count, setCount] = useState(0); - return
    {count}
    ; -} - -export default MyComponent; diff --git a/update-react-imports/tests/no-react-import/input.tsx b/update-react-imports/tests/no-react-import/input.tsx deleted file mode 100644 index a8baecb..0000000 --- a/update-react-imports/tests/no-react-import/input.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { useState } from "react"; - -function MyComponent() { - const [count, setCount] = useState(0); - return
    {count}
    ; -} - -export default MyComponent; diff --git a/update-react-imports/tests/react-fragment-member/expected.tsx b/update-react-imports/tests/react-fragment-member/expected.tsx deleted file mode 100644 index e51ae5c..0000000 --- a/update-react-imports/tests/react-fragment-member/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Fragment } from "react"; - -function MyComponent() { - return ( - -
    Hello
    -
    - ); -} - -export default MyComponent; diff --git a/update-react-imports/tests/react-fragment-member/input.tsx b/update-react-imports/tests/react-fragment-member/input.tsx deleted file mode 100644 index 67e335c..0000000 --- a/update-react-imports/tests/react-fragment-member/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; - -function MyComponent() { - return ( - -
    Hello
    -
    - ); -} - -export default MyComponent; diff --git a/update-react-imports/tsconfig.json b/update-react-imports/tsconfig.json deleted file mode 100644 index 469fc5a..0000000 --- a/update-react-imports/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], - "allowImportingTsExtensions": true, - "noEmit": true, - "verbatimModuleSyntax": true, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": ["tests"] -} diff --git a/use-context-hook/README.md b/use-context-hook/README.md deleted file mode 100644 index 706eeda..0000000 --- a/use-context-hook/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# @react-codemods/use-context-hook - -Transform React.useContext() and useContext() to use() hook - -## Installation - -```bash -# Install from registry -codemod run @react-codemods/use-context-hook - -# Or run locally -codemod run -w workflow.yaml -``` - -## Usage - -This codemod transforms tsx code by: - -- Converting `var` declarations to `const`/`let` -- Removing debug statements -- Modernizing syntax patterns - -## Development - -```bash -# Test the transformation -npm test - -# Validate the workflow -codemod validate -w workflow.yaml - -# Publish to registry -codemod login -codemod publish -``` - -## License -MIT diff --git a/use-context-hook/tests/aliased-useContext/expected.tsx b/use-context-hook/tests/aliased-useContext/expected.tsx deleted file mode 100644 index 36abebb..0000000 --- a/use-context-hook/tests/aliased-useContext/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { use as useCtx } from "react"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const theme = useCtx(ThemeContext); - return
    {theme.name}
    ; -} - -export default MyComponent; diff --git a/use-context-hook/tests/aliased-useContext/input.tsx b/use-context-hook/tests/aliased-useContext/input.tsx deleted file mode 100644 index cff772e..0000000 --- a/use-context-hook/tests/aliased-useContext/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext as useCtx } from "react"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const theme = useCtx(ThemeContext); - return
    {theme.name}
    ; -} - -export default MyComponent; diff --git a/use-context-hook/tests/aliased-useContext/metrics.json b/use-context-hook/tests/aliased-useContext/metrics.json deleted file mode 100644 index ce77a02..0000000 --- a/use-context-hook/tests/aliased-useContext/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "use-context-transformations": [ - { - "cardinality": { - "file": "tests/aliased-useContext/input.tsx", - "pattern": "useContext (aliased)" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/use-context-hook/tests/already-use/expected.tsx b/use-context-hook/tests/already-use/expected.tsx deleted file mode 100644 index 96f875b..0000000 --- a/use-context-hook/tests/already-use/expected.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { use } from "react"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const theme = use(ThemeContext); - - return
    {theme.color}
    ; -} diff --git a/use-context-hook/tests/already-use/input.tsx b/use-context-hook/tests/already-use/input.tsx deleted file mode 100644 index 96f875b..0000000 --- a/use-context-hook/tests/already-use/input.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { use } from "react"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const theme = use(ThemeContext); - - return
    {theme.color}
    ; -} diff --git a/use-context-hook/tests/basic-named-import/expected.tsx b/use-context-hook/tests/basic-named-import/expected.tsx deleted file mode 100644 index 656749d..0000000 --- a/use-context-hook/tests/basic-named-import/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { use } from "react"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const theme = use(ThemeContext); - const user = use(UserContext); - - return
    {theme.color}
    ; -} diff --git a/use-context-hook/tests/basic-named-import/input.tsx b/use-context-hook/tests/basic-named-import/input.tsx deleted file mode 100644 index 25477f4..0000000 --- a/use-context-hook/tests/basic-named-import/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext } from "react"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const theme = useContext(ThemeContext); - const user = useContext(UserContext); - - return
    {theme.color}
    ; -} diff --git a/use-context-hook/tests/basic-named-import/metrics.json b/use-context-hook/tests/basic-named-import/metrics.json deleted file mode 100644 index 5d003fa..0000000 --- a/use-context-hook/tests/basic-named-import/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "use-context-transformations": [ - { - "cardinality": { - "file": "tests/basic-named-import/input.tsx", - "pattern": "useContext" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/use-context-hook/tests/mixed-import/expected.tsx b/use-context-hook/tests/mixed-import/expected.tsx deleted file mode 100644 index cdbbe93..0000000 --- a/use-context-hook/tests/mixed-import/expected.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useState, use, useEffect } from "react"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const [count, setCount] = useState(0); - const theme = use(ThemeContext); - - useEffect(() => { - console.log(theme); - }, [theme]); - - return
    {count}
    ; -} diff --git a/use-context-hook/tests/mixed-import/input.tsx b/use-context-hook/tests/mixed-import/input.tsx deleted file mode 100644 index ab3e414..0000000 --- a/use-context-hook/tests/mixed-import/input.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useState, useContext, useEffect } from "react"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const [count, setCount] = useState(0); - const theme = useContext(ThemeContext); - - useEffect(() => { - console.log(theme); - }, [theme]); - - return
    {count}
    ; -} diff --git a/use-context-hook/tests/no-context/expected.tsx b/use-context-hook/tests/no-context/expected.tsx deleted file mode 100644 index bf75a9c..0000000 --- a/use-context-hook/tests/no-context/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { useState } from "react"; - -function MyComponent() { - const [count, setCount] = useState(0); - - return
    {count}
    ; -} diff --git a/use-context-hook/tests/no-context/input.tsx b/use-context-hook/tests/no-context/input.tsx deleted file mode 100644 index bf75a9c..0000000 --- a/use-context-hook/tests/no-context/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { useState } from "react"; - -function MyComponent() { - const [count, setCount] = useState(0); - - return
    {count}
    ; -} diff --git a/use-context-hook/tests/original-any-use-context/expected.tsx b/use-context-hook/tests/original-any-use-context/expected.tsx deleted file mode 100644 index 8603179..0000000 --- a/use-context-hook/tests/original-any-use-context/expected.tsx +++ /dev/null @@ -1 +0,0 @@ -const theme = trpc.useContext(); diff --git a/use-context-hook/tests/original-any-use-context/input.tsx b/use-context-hook/tests/original-any-use-context/input.tsx deleted file mode 100644 index 8603179..0000000 --- a/use-context-hook/tests/original-any-use-context/input.tsx +++ /dev/null @@ -1 +0,0 @@ -const theme = trpc.useContext(); diff --git a/use-context-hook/tests/original-ts-use-context/metrics.json b/use-context-hook/tests/original-ts-use-context/metrics.json deleted file mode 100644 index 0298e77..0000000 --- a/use-context-hook/tests/original-ts-use-context/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "use-context-transformations": [ - { - "cardinality": { - "file": "tests/original-ts-use-context/input.tsx", - "pattern": "useContext" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/use-context-hook/tests/useContext-not-from-react/expected.tsx b/use-context-hook/tests/useContext-not-from-react/expected.tsx deleted file mode 100644 index 7ec397a..0000000 --- a/use-context-hook/tests/useContext-not-from-react/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext } from "some-other-lib"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const theme = useContext(ThemeContext); - return
    {theme.name}
    ; -} - -export default MyComponent; diff --git a/use-context-hook/tests/useContext-not-from-react/input.tsx b/use-context-hook/tests/useContext-not-from-react/input.tsx deleted file mode 100644 index 7ec397a..0000000 --- a/use-context-hook/tests/useContext-not-from-react/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { useContext } from "some-other-lib"; -import ThemeContext from "./ThemeContext"; - -function MyComponent() { - const theme = useContext(ThemeContext); - return
    {theme.name}
    ; -} - -export default MyComponent; diff --git a/use-context-hook/tsconfig.json b/use-context-hook/tsconfig.json deleted file mode 100644 index 469fc5a..0000000 --- a/use-context-hook/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": ["@codemod.com/jssg-types"], - "allowImportingTsExtensions": true, - "noEmit": true, - "verbatimModuleSyntax": true, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": ["tests"] -} From 05556505f7db45e8afe660862d55793714948b12 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Fri, 10 Apr 2026 19:47:18 +0200 Subject: [PATCH 02/29] add create-element-to-jsx --- codemods/create-element-to-jsx/README.md | 16 + codemods/create-element-to-jsx/codemod.yaml | 19 + codemods/create-element-to-jsx/package.json | 15 + .../create-element-to-jsx/scripts/codemod.ts | 493 ++++++++++++++++++ .../allow-member-expression/expected.tsx | 5 + .../tests/allow-member-expression/input.tsx | 3 + .../allow-member-expression/metrics.json | 10 + .../tests/arg-spread/expected.tsx | 9 + .../tests/arg-spread/input.tsx | 4 + .../tests/arg-spread/metrics.json | 10 + .../tests/call-as-children/expected.tsx | 5 + .../tests/call-as-children/input.tsx | 3 + .../tests/call-as-children/metrics.json | 10 + .../call-expression-as-prop/expected.tsx | 5 + .../tests/call-expression-as-prop/input.tsx | 3 + .../call-expression-as-prop/metrics.json | 10 + .../tests/children-literal/expected.tsx | 5 + .../tests/children-literal/input.tsx | 3 + .../tests/children-literal/metrics.json | 10 + .../tests/children-map/expected.tsx | 5 + .../tests/children-map/input.tsx | 7 + .../tests/children-map/metrics.json | 10 + .../children-mixed-empty-string/expected.tsx | 9 + .../children-mixed-empty-string/input.tsx | 11 + .../children-mixed-empty-string/metrics.json | 10 + .../tests/children/expected.tsx | 8 + .../tests/children/input.tsx | 12 + .../tests/children/metrics.json | 10 + .../tests/computed-component/expected.tsx | 5 + .../tests/computed-component/input.tsx | 5 + .../tests/deep-nesting/expected.tsx | 13 + .../tests/deep-nesting/input.tsx | 21 + .../tests/deep-nesting/metrics.json | 10 + .../element-comment-positioning/expected.tsx | 8 + .../element-comment-positioning/input.tsx | 8 + .../element-comment-positioning/metrics.json | 10 + .../tests/escaped-string/expected.tsx | 7 + .../tests/escaped-string/input.tsx | 7 + .../tests/escaped-string/metrics.json | 10 + .../tests/gt-lt-entities/expected.tsx | 5 + .../tests/gt-lt-entities/input.tsx | 3 + .../tests/gt-lt-entities/metrics.json | 10 + .../ignore-bad-capitalization/expected.tsx | 12 + .../tests/ignore-bad-capitalization/input.tsx | 10 + .../ignore-bad-capitalization/metrics.json | 10 + .../tests/literal-prop/expected.tsx | 3 + .../tests/literal-prop/input.tsx | 3 + .../tests/literal-prop/metrics.json | 10 + .../tests/literal-spacing/expected.tsx | 11 + .../tests/literal-spacing/input.tsx | 15 + .../tests/literal-spacing/metrics.json | 10 + .../member-expression-as-prop/expected.tsx | 5 + .../tests/member-expression-as-prop/input.tsx | 3 + .../member-expression-as-prop/metrics.json | 10 + .../tests/no-props-arg/expected.tsx | 4 + .../tests/no-props-arg/input.tsx | 4 + .../tests/no-props-arg/metrics.json | 10 + .../tests/no-react/expected.tsx | 3 + .../tests/no-react/input.tsx | 3 + .../tests/object-assign/expected.tsx | 3 + .../tests/object-assign/input.tsx | 7 + .../tests/object-assign/metrics.json | 10 + .../tests/preserve-comments/expected.tsx | 77 +++ .../tests/preserve-comments/input.tsx | 38 ++ .../tests/preserve-comments/metrics.json | 10 + .../tests/props-array/expected.tsx | 3 + .../tests/props-array/input.tsx | 8 + .../tests/props-array/metrics.json | 10 + .../tests/props-boolean/expected.tsx | 4 + .../tests/props-boolean/input.tsx | 4 + .../tests/props-boolean/metrics.json | 10 + .../tests/props/expected.tsx | 6 + .../tests/props/input.tsx | 6 + .../tests/props/metrics.json | 10 + .../tests/react-spread/expected.tsx | 3 + .../tests/react-spread/input.tsx | 7 + .../tests/react-spread/metrics.json | 10 + .../tests/single-element/expected.tsx | 4 + .../tests/single-element/input.tsx | 4 + .../tests/single-element/metrics.json | 10 + .../tests/spread-props/expected.tsx | 3 + .../tests/spread-props/input.tsx | 3 + .../tests/spread-props/metrics.json | 10 + .../tests/spread/expected.tsx | 3 + .../tests/spread/input.tsx | 3 + .../tests/spread/metrics.json | 10 + codemods/create-element-to-jsx/tsconfig.json | 27 + codemods/create-element-to-jsx/workflow.yaml | 13 + package.json | 1 + 89 files changed, 1285 insertions(+) create mode 100644 codemods/create-element-to-jsx/README.md create mode 100644 codemods/create-element-to-jsx/codemod.yaml create mode 100644 codemods/create-element-to-jsx/package.json create mode 100644 codemods/create-element-to-jsx/scripts/codemod.ts create mode 100644 codemods/create-element-to-jsx/tests/allow-member-expression/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/allow-member-expression/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/allow-member-expression/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/arg-spread/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/arg-spread/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/arg-spread/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/call-as-children/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/call-as-children/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/call-as-children/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/call-expression-as-prop/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/call-expression-as-prop/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/call-expression-as-prop/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/children-literal/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/children-literal/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/children-literal/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/children-map/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/children-map/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/children-map/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/children-mixed-empty-string/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/children-mixed-empty-string/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/children-mixed-empty-string/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/children/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/children/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/children/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/computed-component/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/computed-component/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/deep-nesting/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/deep-nesting/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/deep-nesting/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/element-comment-positioning/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/element-comment-positioning/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/element-comment-positioning/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/escaped-string/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/escaped-string/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/escaped-string/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/gt-lt-entities/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/gt-lt-entities/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/gt-lt-entities/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/ignore-bad-capitalization/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/ignore-bad-capitalization/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/ignore-bad-capitalization/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/literal-prop/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/literal-prop/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/literal-prop/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/literal-spacing/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/literal-spacing/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/literal-spacing/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/member-expression-as-prop/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/member-expression-as-prop/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/member-expression-as-prop/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/no-props-arg/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/no-props-arg/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/no-props-arg/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/no-react/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/no-react/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/object-assign/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/object-assign/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/object-assign/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/preserve-comments/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/preserve-comments/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/props-array/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/props-array/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/props-array/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/props-boolean/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/props-boolean/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/props-boolean/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/props/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/props/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/props/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/react-spread/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/react-spread/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/react-spread/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/single-element/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/single-element/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/single-element/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/spread-props/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/spread-props/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/spread-props/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/spread/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/spread/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/spread/metrics.json create mode 100644 codemods/create-element-to-jsx/tsconfig.json create mode 100644 codemods/create-element-to-jsx/workflow.yaml diff --git a/codemods/create-element-to-jsx/README.md b/codemods/create-element-to-jsx/README.md new file mode 100644 index 0000000..31f2cae --- /dev/null +++ b/codemods/create-element-to-jsx/README.md @@ -0,0 +1,16 @@ +# @react/create-element-to-jsx + +Convert `React.createElement(...)` calls to JSX. + +## Usage + +```bash +codemod run @react/create-element-to-jsx +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/create-element-to-jsx/codemod.yaml b/codemods/create-element-to-jsx/codemod.yaml new file mode 100644 index 0000000..3b0f069 --- /dev/null +++ b/codemods/create-element-to-jsx/codemod.yaml @@ -0,0 +1,19 @@ +schema_version: "1.0" + +name: "@react/create-element-to-jsx" +version: "0.1.0" +description: "Convert React.createElement calls to JSX" +author: "Codemod " +license: "MIT" +workflow: "workflow.yaml" + +targets: + languages: ["tsx"] + +keywords: ["React", "JSX"] + +registry: + access: "public" + visibility: "public" + +capabilities: [] diff --git a/codemods/create-element-to-jsx/package.json b/codemods/create-element-to-jsx/package.json new file mode 100644 index 0000000..86bfbf6 --- /dev/null +++ b/codemods/create-element-to-jsx/package.json @@ -0,0 +1,15 @@ +{ + "name": "@react/create-element-to-jsx", + "version": "0.1.0", + "description": "Convert React.createElement calls to JSX", + "type": "module", + "scripts": { + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@codemod.com/jssg-types": "latest", + "@types/node": "latest", + "typescript": "latest" + } +} diff --git a/codemods/create-element-to-jsx/scripts/codemod.ts b/codemods/create-element-to-jsx/scripts/codemod.ts new file mode 100644 index 0000000..777f882 --- /dev/null +++ b/codemods/create-element-to-jsx/scripts/codemod.ts @@ -0,0 +1,493 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; + +type Conversion = { + code: string; + count: number; + hasComments: boolean; +}; + +const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + +function lineIndent(source: string, index: number): string { + let lineStart = index; + while (lineStart > 0 && source[lineStart - 1] !== "\n") { + lineStart--; + } + + let end = lineStart; + while (end < source.length && (source[end] === " " || source[end] === "\t")) { + end++; + } + + return source.slice(lineStart, end); +} + +function indentAll(text: string, indent: string): string { + return text + .split("\n") + .map((line) => `${indent}${line}`) + .join("\n"); +} + +function prefixFirstLine(text: string, prefix: string): string { + const newline = text.indexOf("\n"); + if (newline === -1) { + return `${prefix}${text}`; + } + + return `${prefix}${text.slice(0, newline)}${text.slice(newline)}`; +} + +function startsLowercase(value: string): boolean { + const first = value[0]; + return first !== undefined && first >= "a" && first <= "z"; +} + +function encodeJSXTextValue(value: string): string { + return value.replaceAll("<", "<").replaceAll(">", ">"); +} + +function stringValue(node: SgNode): string { + if (node.kind() !== "string") { + throw new Error(`Expected string node, got "${node.kind()}"`); + } + + const evaluated = Function(`"use strict"; return (${node.text()});`)() as unknown; + if (typeof evaluated !== "string") { + throw new Error(`Expected string literal, got "${typeof evaluated}"`); + } + + return evaluated; +} + +function isReactModuleName(value: string | null): boolean { + return value !== null && REACT_MODULES.has(value); +} + +function stringNodeFrom(node: SgNode): SgNode | null { + if (node.kind() === "string") { + return node; + } + + return node.find({ rule: { kind: "string" } }); +} + +function requireSource(call: SgNode): string | null { + const callee = call.field("function"); + if (!callee || callee.kind() !== "identifier" || callee.text() !== "require") { + return null; + } + + const args = callArguments(call); + const firstArg = args[0]; + if (!firstArg || firstArg.kind() !== "string") { + return null; + } + + return stringValue(firstArg); +} + +function importSource(node: SgNode): string | null { + const sourceNode = node.field("source") ?? stringNodeFrom(node); + if (!sourceNode || sourceNode.kind() !== "string") { + return null; + } + + return stringValue(sourceNode); +} + +function hasReact(rootNode: SgNode): boolean { + for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (isReactModuleName(importSource(importNode))) { + return true; + } + } + + for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { + if (isReactModuleName(requireSource(call))) { + return true; + } + } + + return false; +} + +function callArguments(call: SgNode): SgNode[] { + const args = call.field("arguments"); + if (!args) { + return []; + } + + return args.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function isReactCreateElementCall(node: SgNode): boolean { + if (node.kind() !== "call_expression") { + return false; + } + + const callee = node.field("function"); + if (!callee || callee.kind() !== "member_expression") { + return false; + } + + const object = callee.field("object"); + const property = callee.field("property"); + + return object?.kind() === "identifier" && + object.text() === "React" && + property?.kind() === "property_identifier" && + property.text() === "createElement"; +} + +function closestCreateElementAncestor(node: SgNode): SgNode | null { + for (const ancestor of node.ancestors()) { + if (isReactCreateElementCall(ancestor)) { + return ancestor; + } + } + + return null; +} + +function ownCommentTexts(call: SgNode): string[] { + return call.findAll({ rule: { kind: "comment" } }) + .filter((comment) => closestCreateElementAncestor(comment)?.id() === call.id()) + .sort((left, right) => left.range().start.index - right.range().start.index) + .map((comment) => comment.text()); +} + +function jsxReference(node: SgNode): string | null { + if (node.kind() === "identifier" || node.kind() === "property_identifier") { + return node.text(); + } + + if (node.kind() === "string") { + return stringValue(node); + } + + if (node.kind() !== "member_expression") { + return null; + } + + const object = node.field("object"); + const property = node.field("property"); + if (!object || !property) { + return null; + } + + const objectRef = jsxReference(object); + const propertyRef = jsxReference(property); + if (!objectRef || !propertyRef) { + return null; + } + + return `${objectRef}.${propertyRef}`; +} + +function topLevelJsxTag(node: SgNode): string | null { + const reference = jsxReference(node); + if (!reference) { + return null; + } + + if (node.kind() === "identifier" && startsLowercase(node.text())) { + return null; + } + + if (node.kind() === "string" && !startsLowercase(stringValue(node))) { + return null; + } + + return reference; +} + +function legacyType(node: SgNode): string { + switch (node.kind()) { + case "call_expression": + return "CallExpression"; + case "identifier": + return "Identifier"; + case "member_expression": + return "MemberExpression"; + case "object": + return "ObjectExpression"; + case "spread_element": + return "SpreadElement"; + case "string": + case "number": + case "true": + case "false": + case "null": + return "Literal"; + default: + return node.kind(); + } +} + +function canLiteralBePropString(node: SgNode, value: string): boolean { + return !node.text().includes("\\") && !value.includes("\""); +} + +function spreadArgument(node: SgNode): SgNode | null { + return node.children().find((child) => child.isNamed() && child.kind() !== "comment") ?? null; +} + +function attrName(node: SgNode): string { + if (node.kind() === "property_identifier" || node.kind() === "identifier") { + return node.text(); + } + + if (node.kind() === "string") { + return stringValue(node); + } + + throw new Error(`Unexpected property key type "${legacyType(node)}"`); +} + +function pairToAttribute(pair: SgNode): string { + const key = pair.field("key"); + const value = pair.field("value"); + if (!key || !value) { + throw new Error("Expected object property to have key and value"); + } + + const name = attrName(key); + if (value.kind() === "string") { + const literal = stringValue(value); + if (canLiteralBePropString(value, literal)) { + return `${name}="${literal}"`; + } + } + + return `${name}={${value.text()}}`; +} + +function isMemberCall(node: SgNode, objectName: string, propertyName: string): boolean { + if (node.kind() !== "call_expression") { + return false; + } + + const callee = node.field("function"); + if (!callee || callee.kind() !== "member_expression") { + return false; + } + + const object = callee.field("object"); + const property = callee.field("property"); + + return object?.kind() === "identifier" && + object.text() === objectName && + property?.kind() === "property_identifier" && + property.text() === propertyName; +} + +function propsToAttributes(node: SgNode | null): string[] { + if (!node || node.kind() === "null") { + return []; + } + + if (isMemberCall(node, "React", "__spread") || isMemberCall(node, "Object", "assign")) { + return callArguments(node).flatMap((arg) => propsToAttributes(arg)); + } + + if ( + node.kind() === "identifier" || + node.kind() === "member_expression" || + node.kind() === "call_expression" + ) { + return [`{...${node.text()}}`]; + } + + if (node.kind() !== "object") { + throw new Error(`Unexpected attribute of type "${legacyType(node)}"`); + } + + const attributes: string[] = []; + for (const child of node.children()) { + if (!child.isNamed()) { + continue; + } + + if (child.kind() === "comment") { + continue; + } + + if (child.kind() === "spread_element") { + const argument = spreadArgument(child); + if (!argument) { + throw new Error("Expected spread element argument"); + } + + attributes.push(`{...${argument.text()}}`); + continue; + } + + if (child.kind() === "pair") { + attributes.push(pairToAttribute(child)); + } + } + + return attributes; +} + +function renderExpression(code: string, baseIndent: string): string { + if (!code.includes("\n")) { + return `{${code}}`; + } + + return `{\n${indentAll(code, `${baseIndent} `)}\n${baseIndent}}`; +} + +function renderChild(node: SgNode, baseIndent: string): { code: string; count: number } { + if (node.kind() === "string") { + const value = stringValue(node); + if (value !== "" && value.trim() === value) { + return { code: encodeJSXTextValue(value), count: 0 }; + } + + return { code: renderExpression(node.text(), baseIndent), count: 0 }; + } + + if (node.kind() === "spread_element") { + const argument = spreadArgument(node); + if (!argument) { + throw new Error("Expected spread child argument"); + } + + return { code: renderExpression(argument.text(), baseIndent), count: 0 }; + } + + if (isReactCreateElementCall(node)) { + const nested = convertCall(node, baseIndent); + if (!nested) { + return { code: renderExpression(node.text(), baseIndent), count: 0 }; + } + + return { + code: nested.hasComments ? renderExpression(nested.code, baseIndent) : nested.code, + count: nested.count, + }; + } + + return { code: renderExpression(node.text(), baseIndent), count: 0 }; +} + +function wrapWithComments(jsx: string, comments: string[], baseIndent: string): string { + const innerIndent = `${baseIndent} `; + const commentBlock = comments.map((comment) => `${innerIndent}${comment}`).join("\n"); + return `(\n${commentBlock}\n${indentAll(jsx, innerIndent)}\n${baseIndent})`; +} + +function convertCall(call: SgNode, baseIndent: string): Conversion | null { + const args = callArguments(call); + const elementArg = args[0]; + if (!elementArg) { + return null; + } + + const tag = topLevelJsxTag(elementArg); + if (!tag) { + return null; + } + + const propsArg = args[1] ?? null; + const attributes = propsToAttributes(propsArg); + const childIndent = `${baseIndent} `; + const childResults = args.slice(2).map((child) => renderChild(child, childIndent)); + const attributeSuffix = attributes.length > 0 ? ` ${attributes.join(" ")}` : ""; + + let jsx = `<${tag}${attributeSuffix}`; + let count = 1; + for (const child of childResults) { + count += child.count; + } + + if (childResults.length === 0) { + jsx += " />"; + } else { + const childLines = childResults.map((child) => prefixFirstLine(child.code, childIndent)); + jsx += `>\n${childLines.join("\n")}\n${baseIndent}`; + } + + const comments = ownCommentTexts(call); + if (comments.length === 0) { + return { code: jsx, count, hasComments: false }; + } + + return { + code: wrapWithComments(jsx, comments, baseIndent), + count, + hasComments: true, + }; +} + +function explicitRequireDisabled(value: unknown): boolean { + return value === false || value === "false"; +} + +const transform: Transform = async (root, options) => { + const rootNode = root.root(); + const source = rootNode.text(); + const edits: Edit[] = []; + const metric = useMetricAtom("create-element-to-jsx-conversions"); + const convertibility = new Map(); + let totalConversions = 0; + + if (!explicitRequireDisabled(options.params?.["explicit-require"]) && !hasReact(rootNode)) { + return null; + } + + const canConvertCall = (call: SgNode): boolean => { + const cached = convertibility.get(call.id()); + if (cached !== undefined) { + return cached; + } + + const args = callArguments(call); + const elementArg = args[0]; + const convertible = elementArg !== undefined && topLevelJsxTag(elementArg) !== null; + convertibility.set(call.id(), convertible); + return convertible; + }; + + const createElementCalls = rootNode.findAll({ rule: { kind: "call_expression" } }) + .filter(isReactCreateElementCall); + + for (const call of createElementCalls) { + const coveredByAncestor = call.ancestors().some((ancestor) => + isReactCreateElementCall(ancestor) && canConvertCall(ancestor) + ); + if (coveredByAncestor) { + continue; + } + + const baseIndent = lineIndent(source, call.range().start.index); + const conversion = convertCall(call, baseIndent); + if (!conversion) { + continue; + } + + edits.push(call.replace(conversion.code)); + totalConversions += conversion.count; + } + + if (edits.length === 0) { + return null; + } + + metric.increment({ + file: metricFile(root.filename()), + }, totalConversions); + + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/create-element-to-jsx/tests/allow-member-expression/expected.tsx b/codemods/create-element-to-jsx/tests/allow-member-expression/expected.tsx new file mode 100644 index 0000000..bfa842b --- /dev/null +++ b/codemods/create-element-to-jsx/tests/allow-member-expression/expected.tsx @@ -0,0 +1,5 @@ +var React = require('react/addons'); + + + {''} +; diff --git a/codemods/create-element-to-jsx/tests/allow-member-expression/input.tsx b/codemods/create-element-to-jsx/tests/allow-member-expression/input.tsx new file mode 100644 index 0000000..2a78a80 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/allow-member-expression/input.tsx @@ -0,0 +1,3 @@ +var React = require('react/addons'); + +React.createElement(React.addons.CSSTransitionGroup, null, ''); diff --git a/codemods/create-element-to-jsx/tests/allow-member-expression/metrics.json b/codemods/create-element-to-jsx/tests/allow-member-expression/metrics.json new file mode 100644 index 0000000..d3b422e --- /dev/null +++ b/codemods/create-element-to-jsx/tests/allow-member-expression/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/allow-member-expression/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/arg-spread/expected.tsx b/codemods/create-element-to-jsx/tests/arg-spread/expected.tsx new file mode 100644 index 0000000..75b2136 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/arg-spread/expected.tsx @@ -0,0 +1,9 @@ +var React = require('React'); + + + {children} +; + + {firstChild} + {otherChildren} +; diff --git a/codemods/create-element-to-jsx/tests/arg-spread/input.tsx b/codemods/create-element-to-jsx/tests/arg-spread/input.tsx new file mode 100644 index 0000000..c5f23f2 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/arg-spread/input.tsx @@ -0,0 +1,4 @@ +var React = require('React'); + +React.createElement(Foo, null, ...children); +React.createElement(Foo, null, firstChild, ...otherChildren); diff --git a/codemods/create-element-to-jsx/tests/arg-spread/metrics.json b/codemods/create-element-to-jsx/tests/arg-spread/metrics.json new file mode 100644 index 0000000..3c3571e --- /dev/null +++ b/codemods/create-element-to-jsx/tests/arg-spread/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/arg-spread/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/call-as-children/expected.tsx b/codemods/create-element-to-jsx/tests/call-as-children/expected.tsx new file mode 100644 index 0000000..79e558a --- /dev/null +++ b/codemods/create-element-to-jsx/tests/call-as-children/expected.tsx @@ -0,0 +1,5 @@ +var React = require('react/addons'); + +
    + {foo()} +
    ; diff --git a/codemods/create-element-to-jsx/tests/call-as-children/input.tsx b/codemods/create-element-to-jsx/tests/call-as-children/input.tsx new file mode 100644 index 0000000..73dae68 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/call-as-children/input.tsx @@ -0,0 +1,3 @@ +var React = require('react/addons'); + +React.createElement('div', {}, foo()); diff --git a/codemods/create-element-to-jsx/tests/call-as-children/metrics.json b/codemods/create-element-to-jsx/tests/call-as-children/metrics.json new file mode 100644 index 0000000..9b83be3 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/call-as-children/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/call-as-children/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/call-expression-as-prop/expected.tsx b/codemods/create-element-to-jsx/tests/call-expression-as-prop/expected.tsx new file mode 100644 index 0000000..f9cb036 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/call-expression-as-prop/expected.tsx @@ -0,0 +1,5 @@ +var React = require('react/addons'); + +
    + foo +
    ; diff --git a/codemods/create-element-to-jsx/tests/call-expression-as-prop/input.tsx b/codemods/create-element-to-jsx/tests/call-expression-as-prop/input.tsx new file mode 100644 index 0000000..eb81962 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/call-expression-as-prop/input.tsx @@ -0,0 +1,3 @@ +var React = require('react/addons'); + +React.createElement('div', getProps(), 'foo'); diff --git a/codemods/create-element-to-jsx/tests/call-expression-as-prop/metrics.json b/codemods/create-element-to-jsx/tests/call-expression-as-prop/metrics.json new file mode 100644 index 0000000..1901fdb --- /dev/null +++ b/codemods/create-element-to-jsx/tests/call-expression-as-prop/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/call-expression-as-prop/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/children-literal/expected.tsx b/codemods/create-element-to-jsx/tests/children-literal/expected.tsx new file mode 100644 index 0000000..92639f8 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children-literal/expected.tsx @@ -0,0 +1,5 @@ +var React = require('React'); + +
    + foo +
    ; diff --git a/codemods/create-element-to-jsx/tests/children-literal/input.tsx b/codemods/create-element-to-jsx/tests/children-literal/input.tsx new file mode 100644 index 0000000..c8500fd --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children-literal/input.tsx @@ -0,0 +1,3 @@ +var React = require('React'); + +React.createElement('div', null, 'foo'); diff --git a/codemods/create-element-to-jsx/tests/children-literal/metrics.json b/codemods/create-element-to-jsx/tests/children-literal/metrics.json new file mode 100644 index 0000000..bff6a4f --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children-literal/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/children-literal/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/children-map/expected.tsx b/codemods/create-element-to-jsx/tests/children-map/expected.tsx new file mode 100644 index 0000000..90817f2 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children-map/expected.tsx @@ -0,0 +1,5 @@ +var React = require('React'); + +
    + {foo.map(function() {})} +
    ; diff --git a/codemods/create-element-to-jsx/tests/children-map/input.tsx b/codemods/create-element-to-jsx/tests/children-map/input.tsx new file mode 100644 index 0000000..ae04dc9 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children-map/input.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +React.createElement( + 'div', + null, + foo.map(function() {}) +); diff --git a/codemods/create-element-to-jsx/tests/children-map/metrics.json b/codemods/create-element-to-jsx/tests/children-map/metrics.json new file mode 100644 index 0000000..2b46493 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children-map/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/children-map/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/children-mixed-empty-string/expected.tsx b/codemods/create-element-to-jsx/tests/children-mixed-empty-string/expected.tsx new file mode 100644 index 0000000..08c1f61 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children-mixed-empty-string/expected.tsx @@ -0,0 +1,9 @@ +var React = require('react'); + +a = 'foo'; + +
    + {a} + {' '} + {a} +
    ; diff --git a/codemods/create-element-to-jsx/tests/children-mixed-empty-string/input.tsx b/codemods/create-element-to-jsx/tests/children-mixed-empty-string/input.tsx new file mode 100644 index 0000000..0e521fe --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children-mixed-empty-string/input.tsx @@ -0,0 +1,11 @@ +var React = require('react'); + +a = 'foo'; + +React.createElement( + 'div', + null, + a, + ' ', + a +); diff --git a/codemods/create-element-to-jsx/tests/children-mixed-empty-string/metrics.json b/codemods/create-element-to-jsx/tests/children-mixed-empty-string/metrics.json new file mode 100644 index 0000000..5bf7bb6 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children-mixed-empty-string/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/children-mixed-empty-string/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/children/expected.tsx b/codemods/create-element-to-jsx/tests/children/expected.tsx new file mode 100644 index 0000000..425289c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children/expected.tsx @@ -0,0 +1,8 @@ +var React = require('React'); + +var a = +
    + + blah + +; diff --git a/codemods/create-element-to-jsx/tests/children/input.tsx b/codemods/create-element-to-jsx/tests/children/input.tsx new file mode 100644 index 0000000..5d32fa9 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children/input.tsx @@ -0,0 +1,12 @@ +var React = require('React'); + +var a = React.createElement( + Foo, + null, + React.createElement('div', { foo: 'bar' }), + React.createElement( + 'span', + null, + 'blah' + ) +); diff --git a/codemods/create-element-to-jsx/tests/children/metrics.json b/codemods/create-element-to-jsx/tests/children/metrics.json new file mode 100644 index 0000000..3737f1c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/children/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/children/input.tsx" + }, + "count": 3 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/computed-component/expected.tsx b/codemods/create-element-to-jsx/tests/computed-component/expected.tsx new file mode 100644 index 0000000..2d91b7c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/computed-component/expected.tsx @@ -0,0 +1,5 @@ +var React = require('React'); + +React.createElement(a[b]); +React.createElement(a[b.c]); +React.createElement(f()); diff --git a/codemods/create-element-to-jsx/tests/computed-component/input.tsx b/codemods/create-element-to-jsx/tests/computed-component/input.tsx new file mode 100644 index 0000000..2d91b7c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/computed-component/input.tsx @@ -0,0 +1,5 @@ +var React = require('React'); + +React.createElement(a[b]); +React.createElement(a[b.c]); +React.createElement(f()); diff --git a/codemods/create-element-to-jsx/tests/deep-nesting/expected.tsx b/codemods/create-element-to-jsx/tests/deep-nesting/expected.tsx new file mode 100644 index 0000000..f555aee --- /dev/null +++ b/codemods/create-element-to-jsx/tests/deep-nesting/expected.tsx @@ -0,0 +1,13 @@ +var React = require('react/addons'); + +
    + + +
    + +
    +
    ; diff --git a/codemods/create-element-to-jsx/tests/deep-nesting/input.tsx b/codemods/create-element-to-jsx/tests/deep-nesting/input.tsx new file mode 100644 index 0000000..99a9a79 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/deep-nesting/input.tsx @@ -0,0 +1,21 @@ +var React = require('react/addons'); + +React.createElement( + 'div', + {}, + React.createElement( + 'span', + {}, + React.createElement( + 'button', + {}, + React.createElement( + 'a', + {href: 'https://www.google.com'}, + React.createElement('audio') + ) + ), + React.createElement('br'), + React.createElement('img') + ) +); diff --git a/codemods/create-element-to-jsx/tests/deep-nesting/metrics.json b/codemods/create-element-to-jsx/tests/deep-nesting/metrics.json new file mode 100644 index 0000000..ab5be8b --- /dev/null +++ b/codemods/create-element-to-jsx/tests/deep-nesting/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/deep-nesting/input.tsx" + }, + "count": 7 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/element-comment-positioning/expected.tsx b/codemods/create-element-to-jsx/tests/element-comment-positioning/expected.tsx new file mode 100644 index 0000000..a28e49d --- /dev/null +++ b/codemods/create-element-to-jsx/tests/element-comment-positioning/expected.tsx @@ -0,0 +1,8 @@ +var React = require('react/addons'); + +function render() { + return ( + // Note that this element needs to be a div. +
    + ); +} diff --git a/codemods/create-element-to-jsx/tests/element-comment-positioning/input.tsx b/codemods/create-element-to-jsx/tests/element-comment-positioning/input.tsx new file mode 100644 index 0000000..a7431ee --- /dev/null +++ b/codemods/create-element-to-jsx/tests/element-comment-positioning/input.tsx @@ -0,0 +1,8 @@ +var React = require('react/addons'); + +function render() { + return React.createElement( + 'div', // Note that this element needs to be a div. + {foo: 'bar'} + ); +} diff --git a/codemods/create-element-to-jsx/tests/element-comment-positioning/metrics.json b/codemods/create-element-to-jsx/tests/element-comment-positioning/metrics.json new file mode 100644 index 0000000..56d512e --- /dev/null +++ b/codemods/create-element-to-jsx/tests/element-comment-positioning/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/element-comment-positioning/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/escaped-string/expected.tsx b/codemods/create-element-to-jsx/tests/escaped-string/expected.tsx new file mode 100644 index 0000000..94e9ebb --- /dev/null +++ b/codemods/create-element-to-jsx/tests/escaped-string/expected.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +; +; +; +; +; diff --git a/codemods/create-element-to-jsx/tests/escaped-string/input.tsx b/codemods/create-element-to-jsx/tests/escaped-string/input.tsx new file mode 100644 index 0000000..1413547 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/escaped-string/input.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +React.createElement(Foo, {bar: 'abc'}); +React.createElement(Foo, {bar: 'a\nbc'}); +React.createElement(Foo, {bar: 'ab\tc'}); +React.createElement(Foo, {bar: 'ab"c'}); +React.createElement(Foo, {bar: "ab'c"}); diff --git a/codemods/create-element-to-jsx/tests/escaped-string/metrics.json b/codemods/create-element-to-jsx/tests/escaped-string/metrics.json new file mode 100644 index 0000000..a4d2834 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/escaped-string/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/escaped-string/input.tsx" + }, + "count": 5 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/gt-lt-entities/expected.tsx b/codemods/create-element-to-jsx/tests/gt-lt-entities/expected.tsx new file mode 100644 index 0000000..acde1b8 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/gt-lt-entities/expected.tsx @@ -0,0 +1,5 @@ +var React = require('React'); + +
    + <> +
    ; diff --git a/codemods/create-element-to-jsx/tests/gt-lt-entities/input.tsx b/codemods/create-element-to-jsx/tests/gt-lt-entities/input.tsx new file mode 100644 index 0000000..aec09d2 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/gt-lt-entities/input.tsx @@ -0,0 +1,3 @@ +var React = require('React'); + +React.createElement('div', null, '\x3C\x3E'); diff --git a/codemods/create-element-to-jsx/tests/gt-lt-entities/metrics.json b/codemods/create-element-to-jsx/tests/gt-lt-entities/metrics.json new file mode 100644 index 0000000..359e1f6 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/gt-lt-entities/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/gt-lt-entities/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/expected.tsx b/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/expected.tsx new file mode 100644 index 0000000..5a036f7 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/expected.tsx @@ -0,0 +1,12 @@ +var React = require('React'); + +; +React.createElement(foo); +React.createElement('Foo'); +; +<_foo />; +React.createElement('_foo'); +; + + {React.createElement(foo)} +; diff --git a/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/input.tsx b/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/input.tsx new file mode 100644 index 0000000..1e1a8a8 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/input.tsx @@ -0,0 +1,10 @@ +var React = require('React'); + +React.createElement(Foo); +React.createElement(foo); +React.createElement('Foo'); +React.createElement('foo'); +React.createElement(_foo); +React.createElement('_foo'); +React.createElement(foo.bar); +React.createElement(Foo, null, React.createElement(foo)); diff --git a/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/metrics.json b/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/metrics.json new file mode 100644 index 0000000..d414e38 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/ignore-bad-capitalization/input.tsx" + }, + "count": 5 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/literal-prop/expected.tsx b/codemods/create-element-to-jsx/tests/literal-prop/expected.tsx new file mode 100644 index 0000000..12408c0 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/literal-prop/expected.tsx @@ -0,0 +1,3 @@ +var React = require('React'); + +; diff --git a/codemods/create-element-to-jsx/tests/literal-prop/input.tsx b/codemods/create-element-to-jsx/tests/literal-prop/input.tsx new file mode 100644 index 0000000..6b74002 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/literal-prop/input.tsx @@ -0,0 +1,3 @@ +var React = require('React'); + +React.createElement(Foo, {'foo': 'bar'}); diff --git a/codemods/create-element-to-jsx/tests/literal-prop/metrics.json b/codemods/create-element-to-jsx/tests/literal-prop/metrics.json new file mode 100644 index 0000000..d9ec56c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/literal-prop/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/literal-prop/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/literal-spacing/expected.tsx b/codemods/create-element-to-jsx/tests/literal-spacing/expected.tsx new file mode 100644 index 0000000..51ad38b --- /dev/null +++ b/codemods/create-element-to-jsx/tests/literal-spacing/expected.tsx @@ -0,0 +1,11 @@ +var React = require('react/addons'); + +const recipient = 'world'; + + {'Hello '} + {recipient} +; + + Water + {recipient} +; diff --git a/codemods/create-element-to-jsx/tests/literal-spacing/input.tsx b/codemods/create-element-to-jsx/tests/literal-spacing/input.tsx new file mode 100644 index 0000000..1787b2f --- /dev/null +++ b/codemods/create-element-to-jsx/tests/literal-spacing/input.tsx @@ -0,0 +1,15 @@ +var React = require('react/addons'); + +const recipient = 'world'; +React.createElement( + 'span', + {}, + 'Hello ', + recipient +); +React.createElement( + 'span', + {}, + 'Water', + recipient +); diff --git a/codemods/create-element-to-jsx/tests/literal-spacing/metrics.json b/codemods/create-element-to-jsx/tests/literal-spacing/metrics.json new file mode 100644 index 0000000..5b0df95 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/literal-spacing/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/literal-spacing/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/member-expression-as-prop/expected.tsx b/codemods/create-element-to-jsx/tests/member-expression-as-prop/expected.tsx new file mode 100644 index 0000000..ed7e1c0 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/member-expression-as-prop/expected.tsx @@ -0,0 +1,5 @@ +var React = require('react/addons'); + +
    + foo +
    ; diff --git a/codemods/create-element-to-jsx/tests/member-expression-as-prop/input.tsx b/codemods/create-element-to-jsx/tests/member-expression-as-prop/input.tsx new file mode 100644 index 0000000..419c71c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/member-expression-as-prop/input.tsx @@ -0,0 +1,3 @@ +var React = require('react/addons'); + +React.createElement('div', this.props, 'foo'); diff --git a/codemods/create-element-to-jsx/tests/member-expression-as-prop/metrics.json b/codemods/create-element-to-jsx/tests/member-expression-as-prop/metrics.json new file mode 100644 index 0000000..76d7ff6 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/member-expression-as-prop/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/member-expression-as-prop/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/no-props-arg/expected.tsx b/codemods/create-element-to-jsx/tests/no-props-arg/expected.tsx new file mode 100644 index 0000000..d68821d --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-props-arg/expected.tsx @@ -0,0 +1,4 @@ +var React = require('React'); + +var a = ; +var b =
    ; diff --git a/codemods/create-element-to-jsx/tests/no-props-arg/input.tsx b/codemods/create-element-to-jsx/tests/no-props-arg/input.tsx new file mode 100644 index 0000000..6d918af --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-props-arg/input.tsx @@ -0,0 +1,4 @@ +var React = require('React'); + +var a = React.createElement(Foo); +var b = React.createElement('div'); diff --git a/codemods/create-element-to-jsx/tests/no-props-arg/metrics.json b/codemods/create-element-to-jsx/tests/no-props-arg/metrics.json new file mode 100644 index 0000000..0eed063 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-props-arg/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/no-props-arg/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/no-react/expected.tsx b/codemods/create-element-to-jsx/tests/no-react/expected.tsx new file mode 100644 index 0000000..aa81e70 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-react/expected.tsx @@ -0,0 +1,3 @@ +var React = require('foo'); + +React.createElement(Foo, 'la'); diff --git a/codemods/create-element-to-jsx/tests/no-react/input.tsx b/codemods/create-element-to-jsx/tests/no-react/input.tsx new file mode 100644 index 0000000..aa81e70 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-react/input.tsx @@ -0,0 +1,3 @@ +var React = require('foo'); + +React.createElement(Foo, 'la'); diff --git a/codemods/create-element-to-jsx/tests/object-assign/expected.tsx b/codemods/create-element-to-jsx/tests/object-assign/expected.tsx new file mode 100644 index 0000000..d71e1fe --- /dev/null +++ b/codemods/create-element-to-jsx/tests/object-assign/expected.tsx @@ -0,0 +1,3 @@ +var React = require('react/addons'); + +; diff --git a/codemods/create-element-to-jsx/tests/object-assign/input.tsx b/codemods/create-element-to-jsx/tests/object-assign/input.tsx new file mode 100644 index 0000000..0dc500b --- /dev/null +++ b/codemods/create-element-to-jsx/tests/object-assign/input.tsx @@ -0,0 +1,7 @@ +var React = require('react/addons'); + +React.createElement(Foo, Object.assign({ + 'foo': 'bar', +}, props, { + 'bar': 'foo', +})); diff --git a/codemods/create-element-to-jsx/tests/object-assign/metrics.json b/codemods/create-element-to-jsx/tests/object-assign/metrics.json new file mode 100644 index 0000000..09a364c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/object-assign/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/object-assign/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx b/codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx new file mode 100644 index 0000000..945e2b0 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx @@ -0,0 +1,77 @@ +var React = require('react'); + +const render = () => { + return /*1*/( + /*2*/ + /*3*/ + /*4*/ + /*5*/ + /*6*/ + /*7*/ + /*8*/ + /*9*/ + /*10*/ + /*11*/ + /*12*/ + /*13*/ + /*14*/ + /*15*/ + /*16*/ + //17 + /*18*/ + /*19*/ + /*24*/ + //25 +
    + { + ( + /*20*/ + /*21*/ + /*22*/ + /*23*/ + + ) + } + { + ( + /*26*/ + /*27*/ + + ) + } + { + ( + /*28*/ + /*29*/ + + ) + } +
    + ); +}; + +const render2 = () => { + return ( + // Prop comment. + // Child string comment. +
    + hello +
    + ); +}; + +const render3 = () => { + return ( + // Child element comment. +
    + +
    + ); +}; + +const render4 = () => { + return ( + /* No props to see here! */ + + ); +}; diff --git a/codemods/create-element-to-jsx/tests/preserve-comments/input.tsx b/codemods/create-element-to-jsx/tests/preserve-comments/input.tsx new file mode 100644 index 0000000..2e161ff --- /dev/null +++ b/codemods/create-element-to-jsx/tests/preserve-comments/input.tsx @@ -0,0 +1,38 @@ +var React = require('react'); + +const render = () => { + return /*1*/React/*2*/./*3*/createElement/*4*/( + /*5*/'div'/*6*/,/*7*/ + { + /*8*/className/*9*/: /*10*/'foo'/*11*/,/*12*/ + /*13*/onClick/*14*/:/*15*/ this.handleClick/*16*/, //17 + }/*18*/, + /*19*/React.createElement(/*20*/TodoList/*21*/./*22*/Item/*23*/)/*24*/, //25 + React.createElement( + 'span', + /*26*/getProps()/*27*/ + ), + React.createElement('input', /*28*/null/*29*/) + ); +}; + +const render2 = () => { + return React.createElement( + 'div', { + className: 'foo', // Prop comment. + }, + 'hello' // Child string comment. + ); +}; + +const render3 = () => { + return React.createElement( + 'div', + null, + React.createElement('span') // Child element comment. + ); +}; + +const render4 = () => { + return React.createElement(Foo, {/* No props to see here! */}); +}; diff --git a/codemods/create-element-to-jsx/tests/preserve-comments/metrics.json b/codemods/create-element-to-jsx/tests/preserve-comments/metrics.json new file mode 100644 index 0000000..dfde4e1 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/preserve-comments/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/preserve-comments/input.tsx" + }, + "count": 8 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/props-array/expected.tsx b/codemods/create-element-to-jsx/tests/props-array/expected.tsx new file mode 100644 index 0000000..06cfec7 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/props-array/expected.tsx @@ -0,0 +1,3 @@ +var React = require('react'); + +var foo =
    ; diff --git a/codemods/create-element-to-jsx/tests/props-array/input.tsx b/codemods/create-element-to-jsx/tests/props-array/input.tsx new file mode 100644 index 0000000..c70ff04 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/props-array/input.tsx @@ -0,0 +1,8 @@ +var React = require('react'); + +var foo = React.createElement( + 'div', + { + array: [], + } +); diff --git a/codemods/create-element-to-jsx/tests/props-array/metrics.json b/codemods/create-element-to-jsx/tests/props-array/metrics.json new file mode 100644 index 0000000..666ddb5 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/props-array/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/props-array/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/props-boolean/expected.tsx b/codemods/create-element-to-jsx/tests/props-boolean/expected.tsx new file mode 100644 index 0000000..edc58ea --- /dev/null +++ b/codemods/create-element-to-jsx/tests/props-boolean/expected.tsx @@ -0,0 +1,4 @@ +var React = require('React'); + +var a =
    ; +var a =
    ; diff --git a/codemods/create-element-to-jsx/tests/props-boolean/input.tsx b/codemods/create-element-to-jsx/tests/props-boolean/input.tsx new file mode 100644 index 0000000..5c6c75d --- /dev/null +++ b/codemods/create-element-to-jsx/tests/props-boolean/input.tsx @@ -0,0 +1,4 @@ +var React = require('React'); + +var a = React.createElement('div', { a: true }); +var a = React.createElement('div', { a: 4 }); diff --git a/codemods/create-element-to-jsx/tests/props-boolean/metrics.json b/codemods/create-element-to-jsx/tests/props-boolean/metrics.json new file mode 100644 index 0000000..5cf9562 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/props-boolean/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/props-boolean/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/props/expected.tsx b/codemods/create-element-to-jsx/tests/props/expected.tsx new file mode 100644 index 0000000..97b6d5b --- /dev/null +++ b/codemods/create-element-to-jsx/tests/props/expected.tsx @@ -0,0 +1,6 @@ +var React = require('React'); + +function foo() { + var a = ; + var b =
    ; +} diff --git a/codemods/create-element-to-jsx/tests/props/input.tsx b/codemods/create-element-to-jsx/tests/props/input.tsx new file mode 100644 index 0000000..b8230e8 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/props/input.tsx @@ -0,0 +1,6 @@ +var React = require('React'); + +function foo() { + var a = React.createElement(Foo, { foo: 'bar', bar: this.state.baz }); + var b = React.createElement('div', { foo: 'bar', bar: this.state.baz }); +} diff --git a/codemods/create-element-to-jsx/tests/props/metrics.json b/codemods/create-element-to-jsx/tests/props/metrics.json new file mode 100644 index 0000000..e85cae5 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/props/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/props/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/react-spread/expected.tsx b/codemods/create-element-to-jsx/tests/react-spread/expected.tsx new file mode 100644 index 0000000..d71e1fe --- /dev/null +++ b/codemods/create-element-to-jsx/tests/react-spread/expected.tsx @@ -0,0 +1,3 @@ +var React = require('react/addons'); + +; diff --git a/codemods/create-element-to-jsx/tests/react-spread/input.tsx b/codemods/create-element-to-jsx/tests/react-spread/input.tsx new file mode 100644 index 0000000..1a04287 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/react-spread/input.tsx @@ -0,0 +1,7 @@ +var React = require('react/addons'); + +React.createElement(Foo, React.__spread({ + 'foo': 'bar', +}, props, { + 'bar': 'foo', +})); diff --git a/codemods/create-element-to-jsx/tests/react-spread/metrics.json b/codemods/create-element-to-jsx/tests/react-spread/metrics.json new file mode 100644 index 0000000..af33fc4 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/react-spread/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/react-spread/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/single-element/expected.tsx b/codemods/create-element-to-jsx/tests/single-element/expected.tsx new file mode 100644 index 0000000..d68821d --- /dev/null +++ b/codemods/create-element-to-jsx/tests/single-element/expected.tsx @@ -0,0 +1,4 @@ +var React = require('React'); + +var a = ; +var b =
    ; diff --git a/codemods/create-element-to-jsx/tests/single-element/input.tsx b/codemods/create-element-to-jsx/tests/single-element/input.tsx new file mode 100644 index 0000000..5b0a961 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/single-element/input.tsx @@ -0,0 +1,4 @@ +var React = require('React'); + +var a = React.createElement(Foo, null); +var b = React.createElement('div', null); diff --git a/codemods/create-element-to-jsx/tests/single-element/metrics.json b/codemods/create-element-to-jsx/tests/single-element/metrics.json new file mode 100644 index 0000000..3dc19f2 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/single-element/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/single-element/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/spread-props/expected.tsx b/codemods/create-element-to-jsx/tests/spread-props/expected.tsx new file mode 100644 index 0000000..3ccfe6c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/spread-props/expected.tsx @@ -0,0 +1,3 @@ +var React = require('React'); + +; diff --git a/codemods/create-element-to-jsx/tests/spread-props/input.tsx b/codemods/create-element-to-jsx/tests/spread-props/input.tsx new file mode 100644 index 0000000..e43ee8b --- /dev/null +++ b/codemods/create-element-to-jsx/tests/spread-props/input.tsx @@ -0,0 +1,3 @@ +var React = require('React'); + +React.createElement(Constructor, props); diff --git a/codemods/create-element-to-jsx/tests/spread-props/metrics.json b/codemods/create-element-to-jsx/tests/spread-props/metrics.json new file mode 100644 index 0000000..09ceb40 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/spread-props/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/spread-props/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/spread/expected.tsx b/codemods/create-element-to-jsx/tests/spread/expected.tsx new file mode 100644 index 0000000..bb9c69c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/spread/expected.tsx @@ -0,0 +1,3 @@ +var React = require('React'); + +; diff --git a/codemods/create-element-to-jsx/tests/spread/input.tsx b/codemods/create-element-to-jsx/tests/spread/input.tsx new file mode 100644 index 0000000..87a7393 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/spread/input.tsx @@ -0,0 +1,3 @@ +var React = require('React'); + +React.createElement(Foo, {foo: 'bar', ...someObject}); diff --git a/codemods/create-element-to-jsx/tests/spread/metrics.json b/codemods/create-element-to-jsx/tests/spread/metrics.json new file mode 100644 index 0000000..5fd18ec --- /dev/null +++ b/codemods/create-element-to-jsx/tests/spread/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/spread/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tsconfig.json b/codemods/create-element-to-jsx/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/create-element-to-jsx/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/codemods/create-element-to-jsx/workflow.yaml b/codemods/create-element-to-jsx/workflow.yaml new file mode 100644 index 0000000..8773841 --- /dev/null +++ b/codemods/create-element-to-jsx/workflow.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Scan tsx files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + language: "tsx" diff --git a/package.json b/package.json index 9cfee3b..f534f28 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "test:pure-render-mixin": "pnpm --filter @react/pure-render-mixin test", "test:sort-comp": "pnpm --filter @react/sort-comp test", "test:manual-bind-to-arrow": "pnpm --filter @react/manual-bind-to-arrow test", + "test:create-element-to-jsx": "pnpm --filter @react/create-element-to-jsx test", "check-types": "pnpm -r check-types" }, "keywords": [ From 686746d108927956032753b4b1e3274feefb1ef2 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Fri, 10 Apr 2026 20:25:58 +0200 Subject: [PATCH 03/29] remaining codemods --- codemods/pure-component/README.md | 16 + codemods/pure-component/codemod.yaml | 19 ++ codemods/pure-component/package.json | 15 + codemods/pure-component/scripts/codemod.ts | 296 ++++++++++++++++++ .../pure-component/tests/arrow/expected.tsx | 42 +++ codemods/pure-component/tests/arrow/input.tsx | 53 ++++ .../pure-component/tests/arrow/metrics.json | 10 + .../tests/arrow/test.config.json | 5 + .../pure-component/tests/default/expected.tsx | 73 +++++ .../pure-component/tests/default/input.tsx | 86 +++++ .../pure-component/tests/default/metrics.json | 10 + .../tests/destructuring/expected.tsx | 37 +++ .../tests/destructuring/input.tsx | 51 +++ .../tests/destructuring/metrics.json | 10 + .../tests/destructuring/test.config.json | 5 + codemods/pure-component/tsconfig.json | 27 ++ codemods/pure-component/workflow.yaml | 13 + .../README.md | 16 + .../codemod.yaml | 19 ++ .../package.json | 18 ++ .../scripts/codemod.ts | 168 ++++++++++ .../tests/react-dom-basic-case/expected.tsx | 7 + .../tests/react-dom-basic-case/input.tsx | 7 + .../tests/react-dom-basic-case/metrics.json | 10 + .../expected.tsx | 11 + .../react-dom-deconstructed-import/input.tsx | 11 + .../metrics.json | 10 + .../expected.tsx | 11 + .../input.tsx | 11 + .../metrics.json | 10 + .../expected.tsx | 12 + .../react-dom-deconstructed-require/input.tsx | 12 + .../metrics.json | 10 + .../expected.tsx | 4 + .../input.tsx | 4 + .../react-dom-no-change-import/expected.tsx | 7 + .../react-dom-no-change-import/input.tsx | 7 + .../expected.tsx | 7 + .../input.tsx | 7 + .../expected.tsx | 4 + .../input.tsx | 4 + .../react-dom-no-change-require/expected.tsx | 7 + .../react-dom-no-change-require/input.tsx | 7 + .../tsconfig.json | 27 ++ .../workflow.yaml | 13 + .../react-native-view-prop-types/README.md | 16 + .../react-native-view-prop-types/codemod.yaml | 19 ++ .../react-native-view-prop-types/package.json | 18 ++ .../scripts/codemod.ts | 263 ++++++++++++++++ .../expected.tsx | 12 + .../default-import-multi-reference/input.tsx | 10 + .../metrics.json | 10 + .../expected.tsx | 11 + .../default-import-only-reference/input.tsx | 11 + .../metrics.json | 10 + .../expected.tsx | 21 ++ .../default-require-multi-reference/input.tsx | 19 ++ .../metrics.json | 10 + .../expected.tsx | 13 + .../default-require-only-reference/input.tsx | 13 + .../metrics.json | 10 + .../expected.tsx | 12 + .../input.tsx | 12 + .../metrics.json | 10 + .../expected.tsx | 12 + .../input.tsx | 12 + .../metrics.json | 10 + .../expected.tsx | 16 + .../input.tsx | 12 + .../metrics.json | 10 + .../expected.tsx | 10 + .../input.tsx | 10 + .../metrics.json | 10 + .../expected.tsx | 15 + .../import-flow-type-with-require/input.tsx | 15 + .../metrics.json | 10 + .../tests/multiple-replacements/expected.tsx | 31 ++ .../tests/multiple-replacements/input.tsx | 29 ++ .../tests/multiple-replacements/metrics.json | 10 + .../tests/noop-import/expected.tsx | 6 + .../tests/noop-import/input.tsx | 6 + .../tests/noop-require/expected.tsx | 6 + .../tests/noop-require/input.tsx | 6 + .../tsconfig.json | 27 ++ .../workflow.yaml | 13 + codemods/remove-context-provider/README.md | 16 + codemods/remove-context-provider/codemod.yaml | 19 ++ codemods/remove-context-provider/package.json | 15 + .../scripts/codemod.ts | 58 ++++ .../tests/no-provider/expected.tsx | 9 + .../tests/no-provider/input.tsx | 9 + .../tests/typescript-no-provider/expected.tsx | 9 + .../tests/typescript-no-provider/input.tsx | 9 + .../typescript-with-provider-2/expected.tsx | 9 + .../typescript-with-provider-2/input.tsx | 9 + .../typescript-with-provider-2/metrics.json | 10 + .../typescript-with-provider/expected.tsx | 9 + .../tests/typescript-with-provider/input.tsx | 9 + .../typescript-with-provider/metrics.json | 10 + .../tests/with-provider-2/expected.tsx | 9 + .../tests/with-provider-2/input.tsx | 9 + .../tests/with-provider-2/metrics.json | 10 + .../tests/with-provider/expected.tsx | 9 + .../tests/with-provider/input.tsx | 9 + .../tests/with-provider/metrics.json | 10 + .../remove-context-provider/tsconfig.json | 27 ++ .../remove-context-provider/workflow.yaml | 13 + codemods/remove-forward-ref/README.md | 16 + codemods/remove-forward-ref/codemod.yaml | 19 ++ codemods/remove-forward-ref/package.json | 18 ++ .../remove-forward-ref/scripts/codemod.ts | 230 ++++++++++++++ .../arrow-function-expression/expected.tsx | 8 + .../tests/arrow-function-expression/input.tsx | 5 + .../arrow-function-expression/metrics.json | 10 + .../callee-is-member-expression/expected.tsx | 10 + .../callee-is-member-expression/input.tsx | 5 + .../callee-is-member-expression/metrics.json | 10 + .../tests/forward-ref-import-2/expected.tsx | 10 + .../tests/forward-ref-import-2/input.tsx | 5 + .../tests/forward-ref-import-2/metrics.json | 10 + .../tests/forward-ref-import/expected.tsx | 8 + .../tests/forward-ref-import/input.tsx | 5 + .../tests/forward-ref-import/metrics.json | 10 + .../tests/function-expression/expected.tsx | 8 + .../tests/function-expression/input.tsx | 5 + .../tests/function-expression/metrics.json | 10 + .../tests/props-identifier/expected.tsx | 8 + .../tests/props-identifier/input.tsx | 5 + .../tests/props-identifier/metrics.json | 10 + .../tests/props-object-pattern/expected.tsx | 8 + .../tests/props-object-pattern/input.tsx | 5 + .../tests/props-object-pattern/metrics.json | 10 + .../expected.tsx | 10 + .../typescript-props-type-literal/input.tsx | 7 + .../metrics.json | 10 + .../expected.tsx | 10 + .../input.tsx | 8 + .../metrics.json | 10 + .../expected.tsx | 10 + .../input.tsx | 5 + .../metrics.json | 10 + .../typescript-type-arguments/expected.tsx | 12 + .../tests/typescript-type-arguments/input.tsx | 6 + .../typescript-type-arguments/metrics.json | 10 + codemods/remove-forward-ref/tsconfig.json | 27 ++ codemods/remove-forward-ref/workflow.yaml | 13 + package.json | 5 + 147 files changed, 2882 insertions(+) create mode 100644 codemods/pure-component/README.md create mode 100644 codemods/pure-component/codemod.yaml create mode 100644 codemods/pure-component/package.json create mode 100644 codemods/pure-component/scripts/codemod.ts create mode 100644 codemods/pure-component/tests/arrow/expected.tsx create mode 100644 codemods/pure-component/tests/arrow/input.tsx create mode 100644 codemods/pure-component/tests/arrow/metrics.json create mode 100644 codemods/pure-component/tests/arrow/test.config.json create mode 100644 codemods/pure-component/tests/default/expected.tsx create mode 100644 codemods/pure-component/tests/default/input.tsx create mode 100644 codemods/pure-component/tests/default/metrics.json create mode 100644 codemods/pure-component/tests/destructuring/expected.tsx create mode 100644 codemods/pure-component/tests/destructuring/input.tsx create mode 100644 codemods/pure-component/tests/destructuring/metrics.json create mode 100644 codemods/pure-component/tests/destructuring/test.config.json create mode 100644 codemods/pure-component/tsconfig.json create mode 100644 codemods/pure-component/workflow.yaml create mode 100644 codemods/react-dom-to-react-dom-factories/README.md create mode 100644 codemods/react-dom-to-react-dom-factories/codemod.yaml create mode 100644 codemods/react-dom-to-react-dom-factories/package.json create mode 100644 codemods/react-dom-to-react-dom-factories/scripts/codemod.ts create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/metrics.json create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/metrics.json create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/metrics.json create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/metrics.json create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/input.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tsconfig.json create mode 100644 codemods/react-dom-to-react-dom-factories/workflow.yaml create mode 100644 codemods/react-native-view-prop-types/README.md create mode 100644 codemods/react-native-view-prop-types/codemod.yaml create mode 100644 codemods/react-native-view-prop-types/package.json create mode 100644 codemods/react-native-view-prop-types/scripts/codemod.ts create mode 100644 codemods/react-native-view-prop-types/tests/default-import-multi-reference/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/default-import-multi-reference/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/default-import-multi-reference/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/default-import-only-reference/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/default-import-only-reference/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/default-import-only-reference/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/default-require-multi-reference/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/default-require-multi-reference/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/default-require-multi-reference/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/default-require-only-reference/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/default-require-only-reference/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/default-require-only-reference/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/destructured-import-only-reference/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/destructured-import-only-reference/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/destructured-import-only-reference/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/destructured-require-only-reference/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/destructured-require-only-reference/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/destructured-require-only-reference/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/import-flow-type-with-require/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/import-flow-type-with-require/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/import-flow-type-with-require/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/multiple-replacements/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/multiple-replacements/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/multiple-replacements/metrics.json create mode 100644 codemods/react-native-view-prop-types/tests/noop-import/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/noop-import/input.tsx create mode 100644 codemods/react-native-view-prop-types/tests/noop-require/expected.tsx create mode 100644 codemods/react-native-view-prop-types/tests/noop-require/input.tsx create mode 100644 codemods/react-native-view-prop-types/tsconfig.json create mode 100644 codemods/react-native-view-prop-types/workflow.yaml create mode 100644 codemods/remove-context-provider/README.md create mode 100644 codemods/remove-context-provider/codemod.yaml create mode 100644 codemods/remove-context-provider/package.json create mode 100644 codemods/remove-context-provider/scripts/codemod.ts create mode 100644 codemods/remove-context-provider/tests/no-provider/expected.tsx create mode 100644 codemods/remove-context-provider/tests/no-provider/input.tsx create mode 100644 codemods/remove-context-provider/tests/typescript-no-provider/expected.tsx create mode 100644 codemods/remove-context-provider/tests/typescript-no-provider/input.tsx create mode 100644 codemods/remove-context-provider/tests/typescript-with-provider-2/expected.tsx create mode 100644 codemods/remove-context-provider/tests/typescript-with-provider-2/input.tsx create mode 100644 codemods/remove-context-provider/tests/typescript-with-provider-2/metrics.json create mode 100644 codemods/remove-context-provider/tests/typescript-with-provider/expected.tsx create mode 100644 codemods/remove-context-provider/tests/typescript-with-provider/input.tsx create mode 100644 codemods/remove-context-provider/tests/typescript-with-provider/metrics.json create mode 100644 codemods/remove-context-provider/tests/with-provider-2/expected.tsx create mode 100644 codemods/remove-context-provider/tests/with-provider-2/input.tsx create mode 100644 codemods/remove-context-provider/tests/with-provider-2/metrics.json create mode 100644 codemods/remove-context-provider/tests/with-provider/expected.tsx create mode 100644 codemods/remove-context-provider/tests/with-provider/input.tsx create mode 100644 codemods/remove-context-provider/tests/with-provider/metrics.json create mode 100644 codemods/remove-context-provider/tsconfig.json create mode 100644 codemods/remove-context-provider/workflow.yaml create mode 100644 codemods/remove-forward-ref/README.md create mode 100644 codemods/remove-forward-ref/codemod.yaml create mode 100644 codemods/remove-forward-ref/package.json create mode 100644 codemods/remove-forward-ref/scripts/codemod.ts create mode 100644 codemods/remove-forward-ref/tests/arrow-function-expression/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/arrow-function-expression/input.tsx create mode 100644 codemods/remove-forward-ref/tests/arrow-function-expression/metrics.json create mode 100644 codemods/remove-forward-ref/tests/callee-is-member-expression/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/callee-is-member-expression/input.tsx create mode 100644 codemods/remove-forward-ref/tests/callee-is-member-expression/metrics.json create mode 100644 codemods/remove-forward-ref/tests/forward-ref-import-2/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/forward-ref-import-2/input.tsx create mode 100644 codemods/remove-forward-ref/tests/forward-ref-import-2/metrics.json create mode 100644 codemods/remove-forward-ref/tests/forward-ref-import/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/forward-ref-import/input.tsx create mode 100644 codemods/remove-forward-ref/tests/forward-ref-import/metrics.json create mode 100644 codemods/remove-forward-ref/tests/function-expression/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/function-expression/input.tsx create mode 100644 codemods/remove-forward-ref/tests/function-expression/metrics.json create mode 100644 codemods/remove-forward-ref/tests/props-identifier/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/props-identifier/input.tsx create mode 100644 codemods/remove-forward-ref/tests/props-identifier/metrics.json create mode 100644 codemods/remove-forward-ref/tests/props-object-pattern/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/props-object-pattern/input.tsx create mode 100644 codemods/remove-forward-ref/tests/props-object-pattern/metrics.json create mode 100644 codemods/remove-forward-ref/tests/typescript-props-type-literal/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-props-type-literal/input.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-props-type-literal/metrics.json create mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/input.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/metrics.json create mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/input.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/metrics.json create mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments/input.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments/metrics.json create mode 100644 codemods/remove-forward-ref/tsconfig.json create mode 100644 codemods/remove-forward-ref/workflow.yaml diff --git a/codemods/pure-component/README.md b/codemods/pure-component/README.md new file mode 100644 index 0000000..1a2db23 --- /dev/null +++ b/codemods/pure-component/README.md @@ -0,0 +1,16 @@ +# @react/pure-component + +Convert simple React classes into pure function components. + +## Usage + +```bash +codemod run @react/pure-component +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/pure-component/codemod.yaml b/codemods/pure-component/codemod.yaml new file mode 100644 index 0000000..b68e686 --- /dev/null +++ b/codemods/pure-component/codemod.yaml @@ -0,0 +1,19 @@ +schema_version: "1.0" + +name: "@react/pure-component" +version: "0.1.0" +description: "Convert simple React classes into pure function components" +author: "Codemod " +license: "MIT" +workflow: "workflow.yaml" + +targets: + languages: ["tsx"] + +keywords: ["React", "PureComponent"] + +registry: + access: "public" + visibility: "public" + +capabilities: [] diff --git a/codemods/pure-component/package.json b/codemods/pure-component/package.json new file mode 100644 index 0000000..0db6bf0 --- /dev/null +++ b/codemods/pure-component/package.json @@ -0,0 +1,15 @@ +{ + "name": "@react/pure-component", + "version": "0.1.0", + "description": "Convert simple React classes into pure function components", + "type": "module", + "scripts": { + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@codemod.com/jssg-types": "latest", + "@types/node": "latest", + "typescript": "latest" + } +} diff --git a/codemods/pure-component/scripts/codemod.ts b/codemods/pure-component/scripts/codemod.ts new file mode 100644 index 0000000..72db390 --- /dev/null +++ b/codemods/pure-component/scripts/codemod.ts @@ -0,0 +1,296 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + +function namedChildren(node: SgNode | null): SgNode[] { + if (!node) { + return []; + } + + return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function isReactComponentClass(classDecl: SgNode): boolean { + const heritage = classDecl.find({ rule: { kind: "class_heritage" } }); + if (!heritage) { + return false; + } + + return heritage.findAll({ rule: { kind: "member_expression" } }) + .some((member) => { + const object = member.field("object"); + const property = member.field("property"); + return object?.text() === "React" && + (property?.text() === "Component" || property?.text() === "PureComponent"); + }); +} + +function classBodyMembers(classDecl: SgNode): SgNode[] { + const body = classDecl.find({ rule: { kind: "class_body" } }); + return namedChildren(body).filter((member) => + member.kind() === "method_definition" || member.kind() === "public_field_definition" + ); +} + +function memberName(member: SgNode): string { + return member.field("name")?.text() ?? ""; +} + +function isStaticField(member: SgNode): boolean { + return member.kind() === "public_field_definition" && member.text().startsWith("static "); +} + +function isPropsTypeField(member: SgNode): boolean { + return member.kind() === "public_field_definition" && + !member.text().startsWith("static ") && + memberName(member) === "props"; +} + +function hasRefAttribute(classDecl: SgNode): boolean { + return classDecl.findAll({ + rule: { + kind: "jsx_attribute", + }, + }).some((attr) => namedChildren(attr)[0]?.text() === "ref"); +} + +function renderMethod(classDecl: SgNode): SgNode | null { + return classBodyMembers(classDecl).find((member) => + member.kind() === "method_definition" && memberName(member) === "render" + ) ?? null; +} + +function propsTypeAnnotation(classDecl: SgNode): string { + const propsField = classBodyMembers(classDecl).find(isPropsTypeField); + const annotation = propsField?.find({ rule: { kind: "type_annotation" } }); + return annotation?.text() ?? ""; +} + +function isThisProps(node: SgNode): boolean { + if (node.kind() !== "member_expression") { + return false; + } + + const object = node.field("object"); + const property = node.field("property"); + return object?.kind() === "this" && property?.text() === "props"; +} + +function propsAccesses(body: SgNode): SgNode[] { + return body.findAll({ rule: { kind: "member_expression" } }) + .filter((member) => isThisProps(member.field("object"))); +} + +function bareThisPropsUses(body: SgNode): SgNode[] { + return body.findAll({ rule: { kind: "member_expression" } }) + .filter((member) => { + if (!isThisProps(member)) { + return false; + } + + const parent = member.parent(); + return !(parent?.kind() === "member_expression" && parent.field("object")?.id() === member.id()); + }); +} + +function safeDuplicateDeclarators(body: SgNode): Map> { + const duplicates = new Map>(); + + for (const declarator of body.findAll({ rule: { kind: "variable_declarator" } })) { + const name = declarator.field("name"); + const value = declarator.field("value"); + if (name?.kind() !== "identifier" || value?.kind() !== "member_expression") { + continue; + } + + const object = value.field("object"); + const property = value.field("property"); + if (!isThisProps(object) || property?.text() !== name.text()) { + continue; + } + + duplicates.set(name.text(), declarator); + } + + return duplicates; +} + +function propNames(body: SgNode): string[] { + return Array.from(new Set( + propsAccesses(body) + .map((access) => access.field("property")?.text() ?? "") + .filter((name) => name.length > 0), + )).sort(); +} + +function hasDestructureConflicts(body: SgNode, names: string[], duplicates: Map>): boolean { + const propNameSet = new Set(names); + const safeNames = new Set(duplicates.keys()); + + return body.findAll({ rule: { kind: "identifier" } }).some((identifier) => { + const name = identifier.text(); + if (!propNameSet.has(name) || safeNames.has(name)) { + return false; + } + + const parent = identifier.parent(); + if (!parent) { + return true; + } + + if (parent.kind() === "member_expression" && parent.field("property")?.id() === identifier.id()) { + return false; + } + + const object = parent.kind() === "member_expression" ? parent.field("object") : null; + if (object && isThisProps(object)) { + return false; + } + + return true; + }); +} + +function buildParamText( + names: string[], + annotation: string, + destructuring: boolean, +): string { + if (names.length === 0 && annotation.length === 0) { + return ""; + } + + if (!destructuring) { + return `props${annotation}`; + } + + const pattern = `{ ${names.join(", ")} }`; + return annotation.length > 0 ? `${pattern}${annotation}` : pattern; +} + +function buildStaticAssignments(className: string, classDecl: SgNode): string[] { + return classBodyMembers(classDecl) + .filter(isStaticField) + .map((field) => { + const name = field.field("name")?.text() ?? ""; + const value = field.field("value")?.text() ?? "undefined"; + return `${className}.${name} = ${value};`; + }); +} + +function transformBody( + renderBody: SgNode, + destructure: boolean, + names: string[], + duplicates: Map>, +): string { + const edits: Edit[] = []; + + for (const access of propsAccesses(renderBody)) { + const propName = access.field("property")?.text(); + if (!propName) { + continue; + } + + edits.push(access.replace(destructure ? propName : `props.${propName}`)); + } + + for (const use of bareThisPropsUses(renderBody)) { + edits.push(use.replace("props")); + } + + if (destructure) { + for (const declarator of duplicates.values()) { + const statement = declarator.ancestors().find((ancestor) => + ancestor.kind() === "lexical_declaration" || ancestor.kind() === "variable_declaration" + ); + if (statement) { + edits.push({ + startPos: statement.range().start.index, + endPos: statement.range().end.index, + insertedText: "", + }); + } + } + } + + return renderBody.commitEdits(edits); +} + +function isPureClass(classDecl: SgNode): boolean { + if (!isReactComponentClass(classDecl) || hasRefAttribute(classDecl)) { + return false; + } + + const members = classBodyMembers(classDecl); + const methods = members.filter((member) => member.kind() === "method_definition"); + if (methods.length !== 1 || memberName(methods[0]!) !== "render") { + return false; + } + + return members.every((member) => member.kind() === "method_definition" || isStaticField(member) || isPropsTypeField(member)); +} + +const transform: Transform = async (root, options) => { + const rootNode = root.root(); + const edits: Edit[] = []; + const metric = useMetricAtom("pure-component-conversions"); + const useArrows = options.params?.useArrows === true || options.params?.useArrows === "true"; + const destructuring = options.params?.destructuring === true || options.params?.destructuring === "true"; + let conversions = 0; + + for (const classDecl of rootNode.findAll({ rule: { kind: "class_declaration" } })) { + if (!isPureClass(classDecl)) { + continue; + } + + const className = classDecl.field("name")?.text(); + const render = renderMethod(classDecl); + const renderBody = render?.field("body"); + if (!className || !renderBody) { + continue; + } + + const annotation = propsTypeAnnotation(classDecl); + const names = propNames(renderBody); + const duplicates = safeDuplicateDeclarators(renderBody); + const canDestructure = destructuring && + bareThisPropsUses(renderBody).length === 0 && + !hasDestructureConflicts(renderBody, names, duplicates); + const bodyText = transformBody(renderBody, canDestructure, names, duplicates); + const paramText = buildParamText(names, annotation, canDestructure); + const staticAssignments = buildStaticAssignments(className, classDecl); + + let replacement = ""; + if (useArrows) { + replacement = `const ${className} = (${paramText}) => ${bodyText};`; + } else { + replacement = `function ${className}(${paramText}) ${bodyText}`; + } + + if (staticAssignments.length > 0) { + replacement += `\n\n${staticAssignments.join("\n")}`; + } + + edits.push({ + startPos: classDecl.range().start.index, + endPos: classDecl.range().end.index, + insertedText: replacement, + }); + conversions++; + } + + if (edits.length === 0) { + return null; + } + + metric.increment({ file: metricFile(root.filename()) }, conversions); + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/pure-component/tests/arrow/expected.tsx b/codemods/pure-component/tests/arrow/expected.tsx new file mode 100644 index 0000000..34bf613 --- /dev/null +++ b/codemods/pure-component/tests/arrow/expected.tsx @@ -0,0 +1,42 @@ +'use strict'; + +var React = require('React'); + +function render() { + return
    ; +} + +const Pure = (props) => { + return
    ; + }; + +class Impure extends React.Component { + componentWillMount() { + // such impure + } + render() { + return
    ; + } +} + +const PureWithoutProps = () => { + return
    ; + }; + +const PureWithTypes = (props: { foo: string }) => { + return
    ; + }; + +type Props = { foo: string }; + +const PureWithTypes2 = (props: Props) => { + return
    ; + }; + +const PureWithPropTypes = (props) => { + return
    {props.foo}
    ; + }; + +PureWithPropTypes.propTypes = { foo: React.PropTypes.string }; + +var A = props =>
    ; diff --git a/codemods/pure-component/tests/arrow/input.tsx b/codemods/pure-component/tests/arrow/input.tsx new file mode 100644 index 0000000..32775f3 --- /dev/null +++ b/codemods/pure-component/tests/arrow/input.tsx @@ -0,0 +1,53 @@ +'use strict'; + +var React = require('React'); + +function render() { + return
    ; +} + +class Pure extends React.Component { + render() { + return
    ; + } +} + +class Impure extends React.Component { + componentWillMount() { + // such impure + } + render() { + return
    ; + } +} + +class PureWithoutProps extends React.Component { + render() { + return
    ; + } +} + +class PureWithTypes extends React.Component { + props: { foo: string }; + render() { + return
    ; + } +} + +type Props = { foo: string }; + +class PureWithTypes2 extends React.Component { + props: Props; + render() { + return
    ; + } +} + +class PureWithPropTypes extends React.Component { + static propTypes = { foo: React.PropTypes.string }; + render() { + return
    {this.props.foo}
    ; + } +} + +var A = props =>
    ; diff --git a/codemods/pure-component/tests/arrow/metrics.json b/codemods/pure-component/tests/arrow/metrics.json new file mode 100644 index 0000000..66d8789 --- /dev/null +++ b/codemods/pure-component/tests/arrow/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/arrow/input.tsx" + }, + "count": 5 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/arrow/test.config.json b/codemods/pure-component/tests/arrow/test.config.json new file mode 100644 index 0000000..8646dd5 --- /dev/null +++ b/codemods/pure-component/tests/arrow/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "useArrows": true + } +} diff --git a/codemods/pure-component/tests/default/expected.tsx b/codemods/pure-component/tests/default/expected.tsx new file mode 100644 index 0000000..3418d2c --- /dev/null +++ b/codemods/pure-component/tests/default/expected.tsx @@ -0,0 +1,73 @@ +'use strict'; + +var React = require('React'); + +function render() { + return
    ; +} + +function Pure(props) { + return
    ; +} + +class Impure extends React.Component { + componentWillMount() { + // such impure + } + render() { + return
    ; + } +} + +class ImpureWithRef extends React.Component { + render() { + return ( +
    + +
    + ); + } +} + +function PureWithoutProps() { + return
    ; +} + +function PureWithTypes(props: { foo: string }) { + return
    ; +} + +type Props = { foo: string }; + +function PureWithTypes2(props: Props) { + return
    ; +} + +class ImpureClassProperty extends React.Component { + state = { foo: 2 }; + render() { + return
    ; + } +} + +class ImpureClassPropertyWithTypes extends React.Component { + state: { x: string }; + render() { + return
    ; + } +} + +function PureWithPropTypes(props) { + return
    {props.foo}
    ; +} + +PureWithPropTypes.propTypes = { foo: React.PropTypes.string }; +PureWithPropTypes.foo = 'bar'; + +function PureWithPropTypes2(props: { foo: string }) { + return
    {props.foo}
    ; +} + +PureWithPropTypes2.propTypes = { foo: React.PropTypes.string }; + +var A = props =>
    ; diff --git a/codemods/pure-component/tests/default/input.tsx b/codemods/pure-component/tests/default/input.tsx new file mode 100644 index 0000000..8ee7f80 --- /dev/null +++ b/codemods/pure-component/tests/default/input.tsx @@ -0,0 +1,86 @@ +'use strict'; + +var React = require('React'); + +function render() { + return
    ; +} + +class Pure extends React.Component { + render() { + return
    ; + } +} + +class Impure extends React.Component { + componentWillMount() { + // such impure + } + render() { + return
    ; + } +} + +class ImpureWithRef extends React.Component { + render() { + return ( +
    + +
    + ); + } +} + +class PureWithoutProps extends React.Component { + render() { + return
    ; + } +} + +class PureWithTypes extends React.Component { + props: { foo: string }; + render() { + return
    ; + } +} + +type Props = { foo: string }; + +class PureWithTypes2 extends React.Component { + props: Props; + render() { + return
    ; + } +} + +class ImpureClassProperty extends React.Component { + state = { foo: 2 }; + render() { + return
    ; + } +} + +class ImpureClassPropertyWithTypes extends React.Component { + state: { x: string }; + render() { + return
    ; + } +} + +class PureWithPropTypes extends React.Component { + static propTypes = { foo: React.PropTypes.string }; + static foo = 'bar'; + render() { + return
    {this.props.foo}
    ; + } +} + +class PureWithPropTypes2 extends React.Component { + props: { foo: string }; + static propTypes = { foo: React.PropTypes.string }; + render() { + return
    {this.props.foo}
    ; + } +} + +var A = props =>
    ; diff --git a/codemods/pure-component/tests/default/metrics.json b/codemods/pure-component/tests/default/metrics.json new file mode 100644 index 0000000..71a5466 --- /dev/null +++ b/codemods/pure-component/tests/default/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/default/input.tsx" + }, + "count": 6 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/destructuring/expected.tsx b/codemods/pure-component/tests/destructuring/expected.tsx new file mode 100644 index 0000000..2075c82 --- /dev/null +++ b/codemods/pure-component/tests/destructuring/expected.tsx @@ -0,0 +1,37 @@ +'use strict'; + +var React = require('React'); + +const shadow = 'shadow'; + +function doSomething(props) { return props; } + +function ShouldDestsructure({ foo }) { + return
    ; + } + +function ShouldDestructureAndRemoveDuplicateDeclaration({ bar, bizzaz, foo }) { + const fizz = { buzz: 'buzz' }; + + const baz = bizzaz; + const buzz = fizz.buzz; + return
    ; + } + +function UsesThisDotProps(props) { + doSomething(props); + return
    ; + } + +function DestructuresThisDotProps(props) { + const { bar } = props; + return
    ; + } + +function HasShadowProps(props) { + return
    ; + } + +function PureWithTypes({ foo }: { foo: string }) { + return
    ; + } diff --git a/codemods/pure-component/tests/destructuring/input.tsx b/codemods/pure-component/tests/destructuring/input.tsx new file mode 100644 index 0000000..c9875e8 --- /dev/null +++ b/codemods/pure-component/tests/destructuring/input.tsx @@ -0,0 +1,51 @@ +'use strict'; + +var React = require('React'); + +const shadow = 'shadow'; + +function doSomething(props) { return props; } + +class ShouldDestsructure extends React.Component { + render() { + return
    ; + } +} + +class ShouldDestructureAndRemoveDuplicateDeclaration extends React.Component { + render() { + const fizz = { buzz: 'buzz' }; + const bar = this.props.bar; + const baz = this.props.bizzaz; + const buzz = fizz.buzz; + return
    ; + } +} + +class UsesThisDotProps extends React.Component { + render() { + doSomething(this.props); + return
    ; + } +} + +class DestructuresThisDotProps extends React.Component { + // would be nice to destructure in this case + render() { + const { bar } = this.props; + return
    ; + } +} + +class HasShadowProps extends React.Component { + render() { + return
    ; + } +} + +class PureWithTypes extends React.Component { + props: { foo: string }; + render() { + return
    ; + } +} diff --git a/codemods/pure-component/tests/destructuring/metrics.json b/codemods/pure-component/tests/destructuring/metrics.json new file mode 100644 index 0000000..4f66453 --- /dev/null +++ b/codemods/pure-component/tests/destructuring/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/destructuring/input.tsx" + }, + "count": 6 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/destructuring/test.config.json b/codemods/pure-component/tests/destructuring/test.config.json new file mode 100644 index 0000000..dffb75a --- /dev/null +++ b/codemods/pure-component/tests/destructuring/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "destructuring": true + } +} diff --git a/codemods/pure-component/tsconfig.json b/codemods/pure-component/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/pure-component/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/codemods/pure-component/workflow.yaml b/codemods/pure-component/workflow.yaml new file mode 100644 index 0000000..8773841 --- /dev/null +++ b/codemods/pure-component/workflow.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Scan tsx files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + language: "tsx" diff --git a/codemods/react-dom-to-react-dom-factories/README.md b/codemods/react-dom-to-react-dom-factories/README.md new file mode 100644 index 0000000..09f7e8e --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/README.md @@ -0,0 +1,16 @@ +# @react/react-dom-to-react-dom-factories + +Replace `React.DOM.*(...)` and `DOM.*(...)` factory calls with `createElement(...)`. + +## Usage + +```bash +codemod run @react/react-dom-to-react-dom-factories +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/react-dom-to-react-dom-factories/codemod.yaml b/codemods/react-dom-to-react-dom-factories/codemod.yaml new file mode 100644 index 0000000..51748da --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/codemod.yaml @@ -0,0 +1,19 @@ +schema_version: "1.0" + +name: "@react/react-dom-to-react-dom-factories" +version: "0.1.0" +description: "Replace React.DOM factories with createElement calls" +author: "Codemod " +license: "MIT" +workflow: "workflow.yaml" + +targets: + languages: ["tsx"] + +keywords: ["React", "DOM"] + +registry: + access: "public" + visibility: "public" + +capabilities: [] diff --git a/codemods/react-dom-to-react-dom-factories/package.json b/codemods/react-dom-to-react-dom-factories/package.json new file mode 100644 index 0000000..1c19476 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/package.json @@ -0,0 +1,18 @@ +{ + "name": "@react/react-dom-to-react-dom-factories", + "version": "0.1.0", + "description": "Replace React.DOM factories with createElement calls", + "type": "module", + "scripts": { + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@codemod.com/jssg-types": "latest", + "@types/node": "latest", + "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.2" + } +} diff --git a/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts b/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts new file mode 100644 index 0000000..d6d2408 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts @@ -0,0 +1,168 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; +import { getImport } from "@jssg/utils/javascript/imports"; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + +function namedChildren(node: SgNode | null): SgNode[] { + if (!node) { + return []; + } + + return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function stringValue(node: SgNode): string { + const evaluated = Function(`"use strict"; return (${node.text()});`)() as unknown; + if (typeof evaluated !== "string") { + throw new Error(`Expected string literal, got "${typeof evaluated}"`); + } + + return evaluated; +} + +function callArguments(call: SgNode): SgNode[] { + return namedChildren(call.field("arguments")); +} + +function isReactRequireCall(node: SgNode | null): boolean { + if (!node || node.kind() !== "call_expression") { + return false; + } + + const callee = node.field("function"); + const firstArg = callArguments(node)[0]; + return callee?.kind() === "identifier" && + callee.text() === "require" && + firstArg?.kind() === "string" && + stringValue(firstArg) === "react"; +} + +function isReactIdentifier(node: SgNode | null): boolean { + return node?.kind() === "identifier" && node.text() === "React"; +} + +function isReactDomFactoryMember(callee: SgNode): { factory: string; target: string } | null { + if (callee.kind() !== "member_expression") { + return null; + } + + const property = callee.field("property"); + const object = callee.field("object"); + if (!property || !object || property.kind() !== "property_identifier") { + return null; + } + + if (object.kind() === "identifier" && object.text() === "DOM") { + return { factory: property.text(), target: "createElement" }; + } + + if (object.kind() !== "member_expression") { + return null; + } + + const outerProperty = object.field("property"); + const outerObject = object.field("object"); + if (!outerProperty || !outerObject) { + return null; + } + + if (outerProperty.text() === "DOM" && isReactIdentifier(outerObject)) { + return { factory: property.text(), target: "React.createElement" }; + } + + return null; +} + +function isReactDomDestructurePattern(node: SgNode): boolean { + if (node.kind() !== "object_pattern") { + return false; + } + + const declarator = node.parent(); + if (!declarator || declarator.kind() !== "variable_declarator") { + return false; + } + + const init = declarator.field("value"); + return isReactIdentifier(init) || isReactRequireCall(init); +} + +const transform: Transform = async (root) => { + const rootNode = root.root(); + const edits: Edit[] = []; + const metric = useMetricAtom("react-dom-factory-replacements"); + let callReplacements = 0; + + const reactDomImport = getImport(rootNode, { type: "named", name: "DOM", from: "react" }); + const hasReactDomPattern = rootNode.findAll({ rule: { kind: "object_pattern" } }) + .some(isReactDomDestructurePattern); + const hasReactDomBinding = reactDomImport !== null || hasReactDomPattern; + + for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { + const callee = call.field("function"); + if (!callee) { + continue; + } + + const match = isReactDomFactoryMember(callee); + if (!match) { + continue; + } + + if (match.target === "createElement" && !hasReactDomBinding) { + continue; + } + + const args = callArguments(call); + const argsText = args.map((arg) => arg.text()).join(", "); + const prefix = argsText.length > 0 ? ", " : ""; + edits.push(call.replace(`${match.target}('${match.factory}'${prefix}${argsText})`)); + callReplacements++; + } + + for (const importSpec of rootNode.findAll({ rule: { kind: "import_specifier" } })) { + const imported = importSpec.field("name"); + if (!imported || imported.text() !== "DOM") { + continue; + } + + const importStmt = importSpec.ancestors().find((ancestor) => ancestor.kind() === "import_statement"); + if (!importStmt) { + continue; + } + + const source = importStmt.field("source"); + if (!source || source.kind() !== "string" || stringValue(source) !== "react") { + continue; + } + + edits.push(imported.replace("createElement")); + } + + for (const shorthand of rootNode.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } })) { + if (shorthand.text() !== "DOM") { + continue; + } + + const pattern = shorthand.ancestors().find((ancestor) => ancestor.kind() === "object_pattern"); + if (!pattern || !isReactDomDestructurePattern(pattern)) { + continue; + } + + edits.push(shorthand.replace("createElement")); + } + + if (edits.length === 0) { + return null; + } + + metric.increment({ file: metricFile(root.filename()) }, callReplacements); + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/expected.tsx new file mode 100644 index 0000000..fdc8bc6 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/expected.tsx @@ -0,0 +1,7 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/input.tsx new file mode 100644 index 0000000..7543d34 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/input.tsx @@ -0,0 +1,7 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.DOM.div(null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/metrics.json new file mode 100644 index 0000000..6b0f717 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/metrics.json @@ -0,0 +1,10 @@ +{ + "react-dom-factory-replacements": [ + { + "cardinality": { + "file": "tests/react-dom-basic-case/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/expected.tsx new file mode 100644 index 0000000..9be5131 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/expected.tsx @@ -0,0 +1,11 @@ +import ReactDOM from 'ReactDOM'; +import { + Component, + createElement +} from 'react'; + +class Hello extends Component { + render() { + return createElement('div', null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/input.tsx new file mode 100644 index 0000000..32a4b2c --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/input.tsx @@ -0,0 +1,11 @@ +import ReactDOM from 'ReactDOM'; +import { + Component, + DOM +} from 'react'; + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/metrics.json new file mode 100644 index 0000000..a44f433 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/metrics.json @@ -0,0 +1,10 @@ +{ + "react-dom-factory-replacements": [ + { + "cardinality": { + "file": "tests/react-dom-deconstructed-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/expected.tsx new file mode 100644 index 0000000..b5c30c2 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/expected.tsx @@ -0,0 +1,11 @@ +const ReactDOM = require('ReactDOM'); +const { + Component, + createElement +} = require('react'); + +class Hello extends Component { + render() { + return createElement('div', null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/input.tsx new file mode 100644 index 0000000..dd4bec3 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/input.tsx @@ -0,0 +1,11 @@ +const ReactDOM = require('ReactDOM'); +const { + Component, + DOM +} = require('react'); + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/metrics.json new file mode 100644 index 0000000..8528c73 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/metrics.json @@ -0,0 +1,10 @@ +{ + "react-dom-factory-replacements": [ + { + "cardinality": { + "file": "tests/react-dom-deconstructed-require-part-two/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/expected.tsx new file mode 100644 index 0000000..a8ba771 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/expected.tsx @@ -0,0 +1,12 @@ +const React = require('react'); +const ReactDOM = require('ReactDOM'); +const { + Component, + createElement +} = React; + +class Hello extends Component { + render() { + return createElement('div', null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/input.tsx new file mode 100644 index 0000000..92706fc --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/input.tsx @@ -0,0 +1,12 @@ +const React = require('react'); +const ReactDOM = require('ReactDOM'); +const { + Component, + DOM +} = React; + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/metrics.json new file mode 100644 index 0000000..437351b --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/metrics.json @@ -0,0 +1,10 @@ +{ + "react-dom-factory-replacements": [ + { + "cardinality": { + "file": "tests/react-dom-deconstructed-require/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/expected.tsx new file mode 100644 index 0000000..b7657cf --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/expected.tsx @@ -0,0 +1,4 @@ +import {DOM} from 'Free'; + +const foo = DOM.div('a', 'b', 'c'); +const bar = Free.DOM.div('a', 'b', 'c'); diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/input.tsx new file mode 100644 index 0000000..b7657cf --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/input.tsx @@ -0,0 +1,4 @@ +import {DOM} from 'Free'; + +const foo = DOM.div('a', 'b', 'c'); +const bar = Free.DOM.div('a', 'b', 'c'); diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/expected.tsx new file mode 100644 index 0000000..3c7cf3f --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/expected.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/input.tsx new file mode 100644 index 0000000..3c7cf3f --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/input.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/expected.tsx new file mode 100644 index 0000000..3cf194d --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/expected.tsx @@ -0,0 +1,7 @@ +DOM = 'this is a test!'; + +foo.DOM = {}; + +foo.DOM.div = () => null; + +const bar = foo.DOM.div('a', 'b', 'c'); \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/input.tsx new file mode 100644 index 0000000..3cf194d --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/input.tsx @@ -0,0 +1,7 @@ +DOM = 'this is a test!'; + +foo.DOM = {}; + +foo.DOM.div = () => null; + +const bar = foo.DOM.div('a', 'b', 'c'); \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/expected.tsx new file mode 100644 index 0000000..14a21a0 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/expected.tsx @@ -0,0 +1,4 @@ +const {DOM} = require('Free'); + +const foo = DOM.div('a', 'b', 'c'); +const bar = Free.DOM.div('a', 'b', 'c'); diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/input.tsx new file mode 100644 index 0000000..14a21a0 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/input.tsx @@ -0,0 +1,4 @@ +const {DOM} = require('Free'); + +const foo = DOM.div('a', 'b', 'c'); +const bar = Free.DOM.div('a', 'b', 'c'); diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/expected.tsx new file mode 100644 index 0000000..fdc8bc6 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/expected.tsx @@ -0,0 +1,7 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/input.tsx new file mode 100644 index 0000000..fdc8bc6 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/input.tsx @@ -0,0 +1,7 @@ +const React = require('react'); + +class Hello extends React.Component { + render() { + return React.createElement('div', null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tsconfig.json b/codemods/react-dom-to-react-dom-factories/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/codemods/react-dom-to-react-dom-factories/workflow.yaml b/codemods/react-dom-to-react-dom-factories/workflow.yaml new file mode 100644 index 0000000..8773841 --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/workflow.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Scan tsx files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + language: "tsx" diff --git a/codemods/react-native-view-prop-types/README.md b/codemods/react-native-view-prop-types/README.md new file mode 100644 index 0000000..d456bb2 --- /dev/null +++ b/codemods/react-native-view-prop-types/README.md @@ -0,0 +1,16 @@ +# @react/react-native-view-prop-types + +Replace `View.propTypes` with `ViewPropTypes`. + +## Usage + +```bash +codemod run @react/react-native-view-prop-types +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/react-native-view-prop-types/codemod.yaml b/codemods/react-native-view-prop-types/codemod.yaml new file mode 100644 index 0000000..3eee70e --- /dev/null +++ b/codemods/react-native-view-prop-types/codemod.yaml @@ -0,0 +1,19 @@ +schema_version: "1.0" + +name: "@react/react-native-view-prop-types" +version: "0.1.0" +description: "Replace View.propTypes with ViewPropTypes" +author: "Codemod " +license: "MIT" +workflow: "workflow.yaml" + +targets: + languages: ["tsx"] + +keywords: ["React Native", "View"] + +registry: + access: "public" + visibility: "public" + +capabilities: [] diff --git a/codemods/react-native-view-prop-types/package.json b/codemods/react-native-view-prop-types/package.json new file mode 100644 index 0000000..694e514 --- /dev/null +++ b/codemods/react-native-view-prop-types/package.json @@ -0,0 +1,18 @@ +{ + "name": "@react/react-native-view-prop-types", + "version": "0.1.0", + "description": "Replace View.propTypes with ViewPropTypes", + "type": "module", + "scripts": { + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@codemod.com/jssg-types": "latest", + "@types/node": "latest", + "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.2" + } +} diff --git a/codemods/react-native-view-prop-types/scripts/codemod.ts b/codemods/react-native-view-prop-types/scripts/codemod.ts new file mode 100644 index 0000000..6cb0d07 --- /dev/null +++ b/codemods/react-native-view-prop-types/scripts/codemod.ts @@ -0,0 +1,263 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + +function namedChildren(node: SgNode | null): SgNode[] { + if (!node) { + return []; + } + + return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function callArguments(call: SgNode): SgNode[] { + return namedChildren(call.field("arguments")); +} + +function stringValue(node: SgNode): string { + const evaluated = Function(`"use strict"; return (${node.text()});`)() as unknown; + if (typeof evaluated !== "string") { + throw new Error(`Expected string literal, got "${typeof evaluated}"`); + } + + return evaluated; +} + +function isRequireCall(node: SgNode | null, source: string): boolean { + if (!node || node.kind() !== "call_expression") { + return false; + } + + const callee = node.field("function"); + const firstArg = callArguments(node)[0]; + return callee?.kind() === "identifier" && + callee.text() === "require" && + firstArg?.kind() === "string" && + stringValue(firstArg) === source; +} + +function isViewPropTypesAccess(node: SgNode): boolean { + if (node.kind() !== "member_expression") { + return false; + } + + const object = node.field("object"); + const property = node.field("property"); + return object?.kind() === "identifier" && + object.text() === "View" && + property?.kind() === "property_identifier" && + property.text() === "propTypes"; +} + +function isViewBindingNode(node: SgNode): boolean { + if (node.text() !== "View") { + return false; + } + + if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) { + return false; + } + + const parent = node.parent(); + if (!parent) { + return false; + } + + if (parent.kind() === "member_expression" && + parent.field("object")?.id() === node.id() && + parent.field("property")?.text() === "propTypes") { + return false; + } + + if (parent.kind() === "member_expression" && parent.field("property")?.id() === node.id()) { + return false; + } + + if (parent.kind() === "variable_declarator" && parent.field("name")?.id() === node.id()) { + return false; + } + + if (parent.kind() === "shorthand_property_identifier_pattern") { + const pattern = parent.ancestors().find((ancestor) => ancestor.kind() === "object_pattern"); + if (pattern?.parent()?.kind() === "variable_declarator") { + return false; + } + } + + return true; +} + +function findHasteImportStatement(rootNode: SgNode): SgNode | null { + for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { + const source = importStmt.field("source"); + if (source?.kind() === "string" && stringValue(source) === "View") { + return importStmt; + } + } + + return null; +} + +function findHasteRequireStatement(rootNode: SgNode): SgNode | null { + for (const declaration of rootNode.findAll({ + rule: { + any: [ + { pattern: "const View = require('View')" }, + { pattern: "let View = require('View')" }, + { pattern: "var View = require('View')" }, + ], + }, + })) { + if (declaration.kind() === "lexical_declaration" || declaration.kind() === "variable_declaration") { + return declaration; + } + } + + return null; +} + +function findReactNativeImportStatement(rootNode: SgNode): SgNode | null { + for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { + const source = importStmt.field("source"); + if (source?.kind() === "string" && stringValue(source) === "react-native") { + const hasView = importStmt.findAll({ rule: { kind: "import_specifier" } }) + .some((specifier) => specifier.field("name")?.text() === "View"); + if (hasView) { + return importStmt; + } + } + } + + return null; +} + +function findReactNativeRequireStatement(rootNode: SgNode): SgNode | null { + for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { + const name = declarator.field("name"); + const value = declarator.field("value"); + if (name?.kind() !== "object_pattern" || !isRequireCall(value, "react-native")) { + continue; + } + + const hasView = name.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) + .some((property) => property.text() === "View"); + if (hasView) { + return declarator.ancestors().find((ancestor) => + ancestor.kind() === "lexical_declaration" || ancestor.kind() === "variable_declaration" + ) ?? null; + } + } + + return null; +} + +function viewImportSpecifier(importStmt: SgNode): SgNode | null { + return importStmt.findAll({ rule: { kind: "import_specifier" } }) + .find((specifier) => specifier.field("name")?.text() === "View") ?? null; +} + +function viewRequireProperty(statement: SgNode): SgNode | null { + return statement.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) + .find((property) => property.text() === "View") ?? null; +} + +function statementInsertionIndex(source: string, node: SgNode): number { + let index = node.range().end.index; + while (index < source.length && (source[index] === ";" || source[index] === "\r" || source[index] === "\n")) { + index++; + } + return index; +} + +const transform: Transform = async (root) => { + const rootNode = root.root(); + const source = rootNode.text(); + const edits: Edit[] = []; + const metric = useMetricAtom("react-native-view-prop-types-replacements"); + + const hasteImportStatement = findHasteImportStatement(rootNode); + const hasteRequireStatement = findHasteRequireStatement(rootNode); + const reactNativeImportStatement = findReactNativeImportStatement(rootNode); + const reactNativeRequireStatement = findReactNativeRequireStatement(rootNode); + + let replacements = 0; + for (const member of rootNode.findAll({ rule: { kind: "member_expression" } })) { + if (!isViewPropTypesAccess(member)) { + continue; + } + + edits.push(member.replace("ViewPropTypes")); + replacements++; + } + + if (replacements === 0) { + return null; + } + + const keepViewBinding = rootNode.findAll({ + rule: { + any: [ + { kind: "identifier" }, + { kind: "property_identifier" }, + ], + }, + }).some(isViewBindingNode); + + if (hasteImportStatement) { + if (keepViewBinding) { + edits.push({ + startPos: hasteImportStatement.range().end.index, + endPos: hasteImportStatement.range().end.index, + insertedText: "\nimport ViewPropTypes from 'ViewPropTypes';", + }); + } else { + edits.push(hasteImportStatement.replace("import ViewPropTypes from 'ViewPropTypes';")); + } + } else if (hasteRequireStatement) { + if (keepViewBinding) { + edits.push({ + startPos: statementInsertionIndex(source, hasteRequireStatement), + endPos: statementInsertionIndex(source, hasteRequireStatement), + insertedText: "\nconst ViewPropTypes = require('ViewPropTypes');\n", + }); + } else { + edits.push(hasteRequireStatement.replace("const ViewPropTypes = require('ViewPropTypes');")); + } + } else if (reactNativeImportStatement) { + const specifier = viewImportSpecifier(reactNativeImportStatement); + if (specifier) { + if (keepViewBinding) { + edits.push({ + startPos: specifier.range().end.index, + endPos: specifier.range().end.index, + insertedText: ", ViewPropTypes", + }); + } else { + edits.push(specifier.replace("ViewPropTypes")); + } + } + } else if (reactNativeRequireStatement) { + const property = viewRequireProperty(reactNativeRequireStatement); + if (property) { + if (keepViewBinding) { + edits.push({ + startPos: property.range().end.index, + endPos: property.range().end.index, + insertedText: ", ViewPropTypes", + }); + } else { + edits.push(property.replace("ViewPropTypes")); + } + } + } + + metric.increment({ file: metricFile(root.filename()) }, replacements); + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/react-native-view-prop-types/tests/default-import-multi-reference/expected.tsx b/codemods/react-native-view-prop-types/tests/default-import-multi-reference/expected.tsx new file mode 100644 index 0000000..fefcf5b --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-import-multi-reference/expected.tsx @@ -0,0 +1,12 @@ +import { PropTypes } from 'react'; +import View from 'View'; + +import ViewPropTypes from 'ViewPropTypes'; + +function Component() { + return ; +} + +Component.propTypes = ViewPropTypes; + +module.exports = Component; diff --git a/codemods/react-native-view-prop-types/tests/default-import-multi-reference/input.tsx b/codemods/react-native-view-prop-types/tests/default-import-multi-reference/input.tsx new file mode 100644 index 0000000..fde8f85 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-import-multi-reference/input.tsx @@ -0,0 +1,10 @@ +import { PropTypes } from 'react'; +import View from 'View'; + +function Component() { + return ; +} + +Component.propTypes = View.propTypes; + +module.exports = Component; diff --git a/codemods/react-native-view-prop-types/tests/default-import-multi-reference/metrics.json b/codemods/react-native-view-prop-types/tests/default-import-multi-reference/metrics.json new file mode 100644 index 0000000..e59f9f4 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-import-multi-reference/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/default-import-multi-reference/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/default-import-only-reference/expected.tsx b/codemods/react-native-view-prop-types/tests/default-import-only-reference/expected.tsx new file mode 100644 index 0000000..0cd4562 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-import-only-reference/expected.tsx @@ -0,0 +1,11 @@ +import { PropTypes } from 'react'; +import Text from 'Text'; +import ViewPropTypes from 'ViewPropTypes'; + +function Component() { + return text; +} + +Component.propTypes = ViewPropTypes; + +module.exports = Component; diff --git a/codemods/react-native-view-prop-types/tests/default-import-only-reference/input.tsx b/codemods/react-native-view-prop-types/tests/default-import-only-reference/input.tsx new file mode 100644 index 0000000..d38b46e --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-import-only-reference/input.tsx @@ -0,0 +1,11 @@ +import { PropTypes } from 'react'; +import Text from 'Text'; +import View from 'View'; + +function Component() { + return text; +} + +Component.propTypes = View.propTypes; + +module.exports = Component; diff --git a/codemods/react-native-view-prop-types/tests/default-import-only-reference/metrics.json b/codemods/react-native-view-prop-types/tests/default-import-only-reference/metrics.json new file mode 100644 index 0000000..f298b7d --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-import-only-reference/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/default-import-only-reference/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/default-require-multi-reference/expected.tsx b/codemods/react-native-view-prop-types/tests/default-require-multi-reference/expected.tsx new file mode 100644 index 0000000..1dc25be --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-require-multi-reference/expected.tsx @@ -0,0 +1,21 @@ +var React = require('React'); +var View = require('View'); + +const ViewPropTypes = require('ViewPropTypes'); + +var PropTypes = React.PropTypes; + +class ASTrackView extends React.Component { + static propTypes = { + style: ViewPropTypes.style, + track: PropTypes.object.isRequired, + }; + + render() { + return ( + + {this.props.track} + + ); + } +} diff --git a/codemods/react-native-view-prop-types/tests/default-require-multi-reference/input.tsx b/codemods/react-native-view-prop-types/tests/default-require-multi-reference/input.tsx new file mode 100644 index 0000000..cdb7389 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-require-multi-reference/input.tsx @@ -0,0 +1,19 @@ +var React = require('React'); +var View = require('View'); + +var PropTypes = React.PropTypes; + +class ASTrackView extends React.Component { + static propTypes = { + style: View.propTypes.style, + track: PropTypes.object.isRequired, + }; + + render() { + return ( + + {this.props.track} + + ); + } +} diff --git a/codemods/react-native-view-prop-types/tests/default-require-multi-reference/metrics.json b/codemods/react-native-view-prop-types/tests/default-require-multi-reference/metrics.json new file mode 100644 index 0000000..8eebc32 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-require-multi-reference/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/default-require-multi-reference/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/default-require-only-reference/expected.tsx b/codemods/react-native-view-prop-types/tests/default-require-only-reference/expected.tsx new file mode 100644 index 0000000..e078e4f --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-require-only-reference/expected.tsx @@ -0,0 +1,13 @@ +const Animated = require('Animated'); +const React = require('React'); +const ViewPropTypes = require('ViewPropTypes'); + +function MyComponent() { + return React.createElement(Animated.View); +} + +MyComponent.propTypes = { + style: ViewPropTypes.style +}; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/default-require-only-reference/input.tsx b/codemods/react-native-view-prop-types/tests/default-require-only-reference/input.tsx new file mode 100644 index 0000000..47e3d95 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-require-only-reference/input.tsx @@ -0,0 +1,13 @@ +const Animated = require('Animated'); +const React = require('React'); +const View = require('View'); + +function MyComponent() { + return React.createElement(Animated.View); +} + +MyComponent.propTypes = { + style: View.propTypes.style +}; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/default-require-only-reference/metrics.json b/codemods/react-native-view-prop-types/tests/default-require-only-reference/metrics.json new file mode 100644 index 0000000..6c4cdbe --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/default-require-only-reference/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/default-require-only-reference/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/expected.tsx b/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/expected.tsx new file mode 100644 index 0000000..02e1c4c --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/expected.tsx @@ -0,0 +1,12 @@ +import { PropTypes } from 'react'; +import { requireNativeComponent, View, ViewPropTypes } from 'react-native'; + +const Foo = requireNativeComponent('Foo'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = ViewPropTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/input.tsx b/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/input.tsx new file mode 100644 index 0000000..9d9b18f --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/input.tsx @@ -0,0 +1,12 @@ +import { PropTypes } from 'react'; +import { requireNativeComponent, View } from 'react-native'; + +const Foo = requireNativeComponent('Foo'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = View.propTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/metrics.json b/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/metrics.json new file mode 100644 index 0000000..f9bfb2c --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/destructured-import-multi-reference/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/expected.tsx b/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/expected.tsx new file mode 100644 index 0000000..852adb8 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/expected.tsx @@ -0,0 +1,12 @@ +import { PropTypes } from 'react'; +import { requireNativeComponent, ViewPropTypes } from 'react-native'; + +const Foo = requireNativeComponent('Foo'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = ViewPropTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/input.tsx b/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/input.tsx new file mode 100644 index 0000000..ee44a7a --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/input.tsx @@ -0,0 +1,12 @@ +import { PropTypes } from 'react'; +import { requireNativeComponent, View } from 'react-native'; + +const Foo = requireNativeComponent('Foo'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = View.propTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/metrics.json b/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/metrics.json new file mode 100644 index 0000000..a530459 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/destructured-import-only-reference/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx b/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx new file mode 100644 index 0000000..82bc166 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx @@ -0,0 +1,16 @@ +const { PropTypes } = require('react'); +const { + requireNativeComponent, + View, + ViewPropTypes +} = require('react-native'); + +const Foo = requireNativeComponent('Foo'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = ViewPropTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/input.tsx b/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/input.tsx new file mode 100644 index 0000000..44fc9fe --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/input.tsx @@ -0,0 +1,12 @@ +const { PropTypes } = require('react'); +const { requireNativeComponent, View } = require('react-native'); + +const Foo = requireNativeComponent('Foo'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = View.propTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/metrics.json b/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/metrics.json new file mode 100644 index 0000000..2b8657f --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/destructured-require-multi-reference/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/expected.tsx b/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/expected.tsx new file mode 100644 index 0000000..f0f2214 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/expected.tsx @@ -0,0 +1,10 @@ +const PropTypes = require('react'); +const { Animated, ViewPropTypes } = require('react-native'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = ViewPropTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/input.tsx b/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/input.tsx new file mode 100644 index 0000000..2cad73a --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/input.tsx @@ -0,0 +1,10 @@ +const PropTypes = require('react'); +const { Animated, View } = require('react-native'); + +function MyComponent(props) { + return ; +} + +MyComponent.propTypes = View.propTypes; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/metrics.json b/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/metrics.json new file mode 100644 index 0000000..b7af569 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/destructured-require-only-reference/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/expected.tsx b/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/expected.tsx new file mode 100644 index 0000000..2264c11 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/expected.tsx @@ -0,0 +1,15 @@ +const Animated = require('Animated'); +const React = require('React'); +const ViewPropTypes = require('ViewPropTypes'); + +import type { Foo } from 'Foo'; + +function MyComponent(): Foo { + return React.createElement(Animated.View); +} + +MyComponent.propTypes = { + style: ViewPropTypes.style +}; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/input.tsx b/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/input.tsx new file mode 100644 index 0000000..f734f33 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/input.tsx @@ -0,0 +1,15 @@ +const Animated = require('Animated'); +const React = require('React'); +const View = require('View'); + +import type { Foo } from 'Foo'; + +function MyComponent(): Foo { + return React.createElement(Animated.View); +} + +MyComponent.propTypes = { + style: View.propTypes.style +}; + +module.exports = MyComponent; diff --git a/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/metrics.json b/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/metrics.json new file mode 100644 index 0000000..dbd793c --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/import-flow-type-with-require/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/multiple-replacements/expected.tsx b/codemods/react-native-view-prop-types/tests/multiple-replacements/expected.tsx new file mode 100644 index 0000000..2da7197 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/multiple-replacements/expected.tsx @@ -0,0 +1,31 @@ +const React = require('React'); +const ScrollView = require('ScrollView'); +const View = require('View'); + +const ViewPropTypes = require('ViewPropTypes'); + +const PropTypes = React.PropTypes; + +class AdsManagerAbstractRow extends React.Component { + static propTypes = { + containerStyle: ViewPropTypes.style, + style: ViewPropTypes.style, + }; + + render() { + let child = null; + if (this.props.child) { + child = ( + + {this.props.child} + + ); + } + + return ( + + {child} + + ); + } +} diff --git a/codemods/react-native-view-prop-types/tests/multiple-replacements/input.tsx b/codemods/react-native-view-prop-types/tests/multiple-replacements/input.tsx new file mode 100644 index 0000000..fcef6a3 --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/multiple-replacements/input.tsx @@ -0,0 +1,29 @@ +const React = require('React'); +const ScrollView = require('ScrollView'); +const View = require('View'); + +const PropTypes = React.PropTypes; + +class AdsManagerAbstractRow extends React.Component { + static propTypes = { + containerStyle: View.propTypes.style, + style: View.propTypes.style, + }; + + render() { + let child = null; + if (this.props.child) { + child = ( + + {this.props.child} + + ); + } + + return ( + + {child} + + ); + } +} diff --git a/codemods/react-native-view-prop-types/tests/multiple-replacements/metrics.json b/codemods/react-native-view-prop-types/tests/multiple-replacements/metrics.json new file mode 100644 index 0000000..6ad8dcd --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/multiple-replacements/metrics.json @@ -0,0 +1,10 @@ +{ + "react-native-view-prop-types-replacements": [ + { + "cardinality": { + "file": "tests/multiple-replacements/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/noop-import/expected.tsx b/codemods/react-native-view-prop-types/tests/noop-import/expected.tsx new file mode 100644 index 0000000..e02fc4a --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/noop-import/expected.tsx @@ -0,0 +1,6 @@ +import React from 'React'; +import View from 'View'; + +module.exports = function Component() { + return ; +}; diff --git a/codemods/react-native-view-prop-types/tests/noop-import/input.tsx b/codemods/react-native-view-prop-types/tests/noop-import/input.tsx new file mode 100644 index 0000000..e02fc4a --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/noop-import/input.tsx @@ -0,0 +1,6 @@ +import React from 'React'; +import View from 'View'; + +module.exports = function Component() { + return ; +}; diff --git a/codemods/react-native-view-prop-types/tests/noop-require/expected.tsx b/codemods/react-native-view-prop-types/tests/noop-require/expected.tsx new file mode 100644 index 0000000..c1bc51a --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/noop-require/expected.tsx @@ -0,0 +1,6 @@ +const React = require('React'); +const View = require('View'); + +module.exports = function Component() { + return ; +}; diff --git a/codemods/react-native-view-prop-types/tests/noop-require/input.tsx b/codemods/react-native-view-prop-types/tests/noop-require/input.tsx new file mode 100644 index 0000000..c1bc51a --- /dev/null +++ b/codemods/react-native-view-prop-types/tests/noop-require/input.tsx @@ -0,0 +1,6 @@ +const React = require('React'); +const View = require('View'); + +module.exports = function Component() { + return ; +}; diff --git a/codemods/react-native-view-prop-types/tsconfig.json b/codemods/react-native-view-prop-types/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/react-native-view-prop-types/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/codemods/react-native-view-prop-types/workflow.yaml b/codemods/react-native-view-prop-types/workflow.yaml new file mode 100644 index 0000000..8773841 --- /dev/null +++ b/codemods/react-native-view-prop-types/workflow.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Scan tsx files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + language: "tsx" diff --git a/codemods/remove-context-provider/README.md b/codemods/remove-context-provider/README.md new file mode 100644 index 0000000..a9ae52c --- /dev/null +++ b/codemods/remove-context-provider/README.md @@ -0,0 +1,16 @@ +# @react/remove-context-provider + +Replace `Context.Provider` JSX tags with `Context`. + +## Usage + +```bash +codemod run @react/remove-context-provider +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/remove-context-provider/codemod.yaml b/codemods/remove-context-provider/codemod.yaml new file mode 100644 index 0000000..1c4b4c9 --- /dev/null +++ b/codemods/remove-context-provider/codemod.yaml @@ -0,0 +1,19 @@ +schema_version: "1.0" + +name: "@react/remove-context-provider" +version: "0.1.0" +description: "Replace Context.Provider with Context in JSX" +author: "Codemod " +license: "MIT" +workflow: "workflow.yaml" + +targets: + languages: ["tsx"] + +keywords: ["React", "Context"] + +registry: + access: "public" + visibility: "public" + +capabilities: [] diff --git a/codemods/remove-context-provider/package.json b/codemods/remove-context-provider/package.json new file mode 100644 index 0000000..37d7f7d --- /dev/null +++ b/codemods/remove-context-provider/package.json @@ -0,0 +1,15 @@ +{ + "name": "@react/remove-context-provider", + "version": "0.1.0", + "description": "Replace Context.Provider with Context in JSX", + "type": "module", + "scripts": { + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@codemod.com/jssg-types": "latest", + "@types/node": "latest", + "typescript": "latest" + } +} diff --git a/codemods/remove-context-provider/scripts/codemod.ts b/codemods/remove-context-provider/scripts/codemod.ts new file mode 100644 index 0000000..10480be --- /dev/null +++ b/codemods/remove-context-provider/scripts/codemod.ts @@ -0,0 +1,58 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + +function isContextProvider(node: SgNode): boolean { + if (node.kind() !== "member_expression") { + return false; + } + + const object = node.field("object"); + const property = node.field("property"); + if (!object || !property || property.text() !== "Provider") { + return false; + } + + return object.text().toLowerCase().includes("context"); +} + +const transform: Transform = async (root) => { + const rootNode = root.root(); + const edits: Edit[] = []; + const metric = useMetricAtom("remove-context-provider-replacements"); + let replacements = 0; + + for (const member of rootNode.findAll({ rule: { kind: "member_expression" } })) { + const parent = member.parent(); + if (!parent) { + continue; + } + + const isJsxName = parent.kind() === "jsx_opening_element" || parent.kind() === "jsx_closing_element"; + if (!isJsxName || !isContextProvider(member)) { + continue; + } + + const object = member.field("object"); + if (!object) { + continue; + } + + edits.push(member.replace(object.text())); + replacements++; + } + + if (edits.length === 0) { + return null; + } + + metric.increment({ file: metricFile(root.filename()) }, replacements); + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/remove-context-provider/tests/no-provider/expected.tsx b/codemods/remove-context-provider/tests/no-provider/expected.tsx new file mode 100644 index 0000000..9ef6573 --- /dev/null +++ b/codemods/remove-context-provider/tests/no-provider/expected.tsx @@ -0,0 +1,9 @@ +function App() { + const [theme, setTheme] = useState('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/no-provider/input.tsx b/codemods/remove-context-provider/tests/no-provider/input.tsx new file mode 100644 index 0000000..9ef6573 --- /dev/null +++ b/codemods/remove-context-provider/tests/no-provider/input.tsx @@ -0,0 +1,9 @@ +function App() { + const [theme, setTheme] = useState('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-no-provider/expected.tsx b/codemods/remove-context-provider/tests/typescript-no-provider/expected.tsx new file mode 100644 index 0000000..357cc7c --- /dev/null +++ b/codemods/remove-context-provider/tests/typescript-no-provider/expected.tsx @@ -0,0 +1,9 @@ +function App({ url }: { url: string }) { + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-no-provider/input.tsx b/codemods/remove-context-provider/tests/typescript-no-provider/input.tsx new file mode 100644 index 0000000..357cc7c --- /dev/null +++ b/codemods/remove-context-provider/tests/typescript-no-provider/input.tsx @@ -0,0 +1,9 @@ +function App({ url }: { url: string }) { + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-with-provider-2/expected.tsx b/codemods/remove-context-provider/tests/typescript-with-provider-2/expected.tsx new file mode 100644 index 0000000..357cc7c --- /dev/null +++ b/codemods/remove-context-provider/tests/typescript-with-provider-2/expected.tsx @@ -0,0 +1,9 @@ +function App({ url }: { url: string }) { + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-with-provider-2/input.tsx b/codemods/remove-context-provider/tests/typescript-with-provider-2/input.tsx new file mode 100644 index 0000000..b8d7261 --- /dev/null +++ b/codemods/remove-context-provider/tests/typescript-with-provider-2/input.tsx @@ -0,0 +1,9 @@ +function App({ url }: { url: string }) { + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-with-provider-2/metrics.json b/codemods/remove-context-provider/tests/typescript-with-provider-2/metrics.json new file mode 100644 index 0000000..d7e29d4 --- /dev/null +++ b/codemods/remove-context-provider/tests/typescript-with-provider-2/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-context-provider-replacements": [ + { + "cardinality": { + "file": "tests/typescript-with-provider-2/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-with-provider/expected.tsx b/codemods/remove-context-provider/tests/typescript-with-provider/expected.tsx new file mode 100644 index 0000000..f1d4569 --- /dev/null +++ b/codemods/remove-context-provider/tests/typescript-with-provider/expected.tsx @@ -0,0 +1,9 @@ +function App({ url }: { url: string }) { + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-with-provider/input.tsx b/codemods/remove-context-provider/tests/typescript-with-provider/input.tsx new file mode 100644 index 0000000..9787205 --- /dev/null +++ b/codemods/remove-context-provider/tests/typescript-with-provider/input.tsx @@ -0,0 +1,9 @@ +function App({ url }: { url: string }) { + const [theme, setTheme] = useState<'light' | 'dark'>('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-with-provider/metrics.json b/codemods/remove-context-provider/tests/typescript-with-provider/metrics.json new file mode 100644 index 0000000..8bcb233 --- /dev/null +++ b/codemods/remove-context-provider/tests/typescript-with-provider/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-context-provider-replacements": [ + { + "cardinality": { + "file": "tests/typescript-with-provider/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/with-provider-2/expected.tsx b/codemods/remove-context-provider/tests/with-provider-2/expected.tsx new file mode 100644 index 0000000..9ef6573 --- /dev/null +++ b/codemods/remove-context-provider/tests/with-provider-2/expected.tsx @@ -0,0 +1,9 @@ +function App() { + const [theme, setTheme] = useState('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/with-provider-2/input.tsx b/codemods/remove-context-provider/tests/with-provider-2/input.tsx new file mode 100644 index 0000000..18cb6e6 --- /dev/null +++ b/codemods/remove-context-provider/tests/with-provider-2/input.tsx @@ -0,0 +1,9 @@ +function App() { + const [theme, setTheme] = useState('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/with-provider-2/metrics.json b/codemods/remove-context-provider/tests/with-provider-2/metrics.json new file mode 100644 index 0000000..d0d0389 --- /dev/null +++ b/codemods/remove-context-provider/tests/with-provider-2/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-context-provider-replacements": [ + { + "cardinality": { + "file": "tests/with-provider-2/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/with-provider/expected.tsx b/codemods/remove-context-provider/tests/with-provider/expected.tsx new file mode 100644 index 0000000..84f317b --- /dev/null +++ b/codemods/remove-context-provider/tests/with-provider/expected.tsx @@ -0,0 +1,9 @@ +function App() { + const [theme, setTheme] = useState('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/with-provider/input.tsx b/codemods/remove-context-provider/tests/with-provider/input.tsx new file mode 100644 index 0000000..a066cc8 --- /dev/null +++ b/codemods/remove-context-provider/tests/with-provider/input.tsx @@ -0,0 +1,9 @@ +function App() { + const [theme, setTheme] = useState('light'); + + return ( + + + + ); +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/with-provider/metrics.json b/codemods/remove-context-provider/tests/with-provider/metrics.json new file mode 100644 index 0000000..10a3c05 --- /dev/null +++ b/codemods/remove-context-provider/tests/with-provider/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-context-provider-replacements": [ + { + "cardinality": { + "file": "tests/with-provider/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-context-provider/tsconfig.json b/codemods/remove-context-provider/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/remove-context-provider/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/codemods/remove-context-provider/workflow.yaml b/codemods/remove-context-provider/workflow.yaml new file mode 100644 index 0000000..8773841 --- /dev/null +++ b/codemods/remove-context-provider/workflow.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Scan tsx files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + language: "tsx" diff --git a/codemods/remove-forward-ref/README.md b/codemods/remove-forward-ref/README.md new file mode 100644 index 0000000..2ecdb85 --- /dev/null +++ b/codemods/remove-forward-ref/README.md @@ -0,0 +1,16 @@ +# @react/remove-forward-ref + +Inline `forwardRef(...)` render functions and pass `ref` through props. + +## Usage + +```bash +codemod run @react/remove-forward-ref +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/remove-forward-ref/codemod.yaml b/codemods/remove-forward-ref/codemod.yaml new file mode 100644 index 0000000..f70874b --- /dev/null +++ b/codemods/remove-forward-ref/codemod.yaml @@ -0,0 +1,19 @@ +schema_version: "1.0" + +name: "@react/remove-forward-ref" +version: "0.1.0" +description: "Inline forwardRef render functions and move ref into props" +author: "Codemod " +license: "MIT" +workflow: "workflow.yaml" + +targets: + languages: ["tsx"] + +keywords: ["React", "forwardRef"] + +registry: + access: "public" + visibility: "public" + +capabilities: [] diff --git a/codemods/remove-forward-ref/package.json b/codemods/remove-forward-ref/package.json new file mode 100644 index 0000000..98a2bbe --- /dev/null +++ b/codemods/remove-forward-ref/package.json @@ -0,0 +1,18 @@ +{ + "name": "@react/remove-forward-ref", + "version": "0.1.0", + "description": "Inline forwardRef render functions and move ref into props", + "type": "module", + "scripts": { + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@codemod.com/jssg-types": "latest", + "@types/node": "latest", + "typescript": "latest" + }, + "dependencies": { + "@jssg/utils": "^0.0.2" + } +} diff --git a/codemods/remove-forward-ref/scripts/codemod.ts b/codemods/remove-forward-ref/scripts/codemod.ts new file mode 100644 index 0000000..a3ab1fc --- /dev/null +++ b/codemods/remove-forward-ref/scripts/codemod.ts @@ -0,0 +1,230 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; +import { getImport, removeImport } from "@jssg/utils/javascript/imports"; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + +function namedChildren(node: SgNode | null): SgNode[] { + if (!node) { + return []; + } + + return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function firstNamedChild(node: SgNode | null): SgNode | null { + return namedChildren(node)[0] ?? null; +} + +function callArguments(call: SgNode): SgNode[] { + return namedChildren(call.field("arguments")); +} + +function functionParameters(fn: SgNode): SgNode[] { + return namedChildren(fn.field("parameters")); +} + +function parameterPattern(param: SgNode): SgNode | null { + return firstNamedChild(param); +} + +function parameterTypeAnnotation(param: SgNode): SgNode | null { + const children = namedChildren(param); + return children.length > 1 ? children[1] ?? null : null; +} + +function stripLeadingColon(text: string): string { + return text.startsWith(":") ? text.slice(1).trim() : text.trim(); +} + +function callTypeArguments(call: SgNode): SgNode[] { + const typeArguments = call.children().find((child) => child.kind() === "type_arguments"); + return namedChildren(typeArguments ?? null); +} + +function refTypeFromForwardedRef(typeAnnotation: SgNode | null): string | null { + const typeNode = firstNamedChild(typeAnnotation); + if (!typeNode || typeNode.kind() !== "generic_type") { + return null; + } + + const children = namedChildren(typeNode); + const typeNameNode = children[0]; + const genericArgs = children[1]; + if (!typeNameNode || !genericArgs) { + return null; + } + + const typeNameChildren = namedChildren(typeNameNode); + const lastTypeName = typeNameChildren[typeNameChildren.length - 1] ?? typeNameNode; + if (lastTypeName.text() !== "ForwardedRef") { + return null; + } + + return firstNamedChild(genericArgs)?.text() ?? null; +} + +function refBindingEntry(refName: string): string { + return refName === "ref" ? "ref" : `ref: ${refName}`; +} + +function objectPatternInnerText(pattern: SgNode): string { + const text = pattern.text().trim(); + return text.startsWith("{") && text.endsWith("}") ? text.slice(1, -1).trim() : text; +} + +function propsPatternText(propsParam: SgNode, refName: string): string | null { + const pattern = parameterPattern(propsParam); + if (!pattern) { + return null; + } + + if (pattern.kind() === "identifier") { + return `{ ${refBindingEntry(refName)}, ...${pattern.text()} }`; + } + + if (pattern.kind() === "object_pattern") { + const inner = objectPatternInnerText(pattern); + return inner.length > 0 + ? `{ ${refBindingEntry(refName)}, ${inner} }` + : `{ ${refBindingEntry(refName)} }`; + } + + return null; +} + +function paramTypeText(propsParam: SgNode, call: SgNode, refParam: SgNode): string | null { + const typeArgs = callTypeArguments(call); + const callRefType = typeArgs[0]?.text() ?? null; + const callPropsType = typeArgs[1]?.text() ?? null; + if (callPropsType) { + return `${callPropsType} & { ref: React.RefObject<${callRefType ?? "unknown"}> }`; + } + + const propsType = parameterTypeAnnotation(propsParam); + if (!propsType) { + return null; + } + + const refType = refTypeFromForwardedRef(parameterTypeAnnotation(refParam)) ?? "unknown"; + return `${stripLeadingColon(propsType.text())} & { ref: React.RefObject<${refType}> }`; +} + +function bodyText(fn: SgNode): string | null { + return fn.field("body")?.text() ?? null; +} + +function functionName(fn: SgNode): string { + return fn.children().find((child) => child.kind() === "identifier")?.text() ?? ""; +} + +function buildReplacement(call: SgNode): string | null { + const renderFunction = callArguments(call)[0]; + if (!renderFunction || (renderFunction.kind() !== "function_expression" && renderFunction.kind() !== "arrow_function")) { + return null; + } + + const params = functionParameters(renderFunction); + const propsParam = params[0]; + const refParam = params[1]; + if (!propsParam || !refParam) { + return null; + } + + const refPattern = parameterPattern(refParam); + if (!refPattern || refPattern.kind() !== "identifier") { + return null; + } + + const propsText = propsPatternText(propsParam, refPattern.text()); + const fnBodyText = bodyText(renderFunction); + if (!propsText || !fnBodyText) { + return null; + } + + const annotation = paramTypeText(propsParam, call, refParam); + const finalParam = annotation ? `${propsText}: ${annotation}` : propsText; + + if (renderFunction.kind() === "function_expression") { + const name = functionName(renderFunction); + const namePart = name.length > 0 ? ` ${name}` : ""; + return `function${namePart}(${finalParam}) ${fnBodyText}`; + } + + return `(${finalParam}) => ${fnBodyText}`; +} + +function isForwardRefIdentifierCall(call: SgNode, localName: string | null): boolean { + if (!localName) { + return false; + } + + const callee = call.field("function"); + return callee?.kind() === "identifier" && callee.text() === localName; +} + +function isForwardRefMemberCall(call: SgNode, reactLocalName: string | null): boolean { + if (!reactLocalName) { + return false; + } + + const callee = call.field("function"); + if (!callee || callee.kind() !== "member_expression") { + return false; + } + + const object = callee.field("object"); + const property = callee.field("property"); + return object?.kind() === "identifier" && + object.text() === reactLocalName && + property?.text() === "forwardRef"; +} + +const transform: Transform = async (root) => { + const rootNode = root.root(); + const edits: Edit[] = []; + const metric = useMetricAtom("remove-forward-ref-replacements"); + + const forwardRefImport = getImport(rootNode, { type: "named", name: "forwardRef", from: "react" }); + const reactImport = getImport(rootNode, { type: "default", from: "react" }); + let replacements = 0; + + for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { + if (!isForwardRefIdentifierCall(call, forwardRefImport?.alias ?? null) && + !isForwardRefMemberCall(call, reactImport?.alias ?? null)) { + continue; + } + + const replacement = buildReplacement(call); + if (!replacement) { + continue; + } + + edits.push(call.replace(replacement)); + replacements++; + } + + if (replacements === 0) { + return null; + } + + if (forwardRefImport) { + const removeEdit = removeImport(rootNode, { + type: "named", + specifiers: ["forwardRef"], + from: "react", + }); + if (removeEdit) { + edits.push(removeEdit); + } + } + + metric.increment({ file: metricFile(root.filename()) }, replacements); + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/remove-forward-ref/tests/arrow-function-expression/expected.tsx b/codemods/remove-forward-ref/tests/arrow-function-expression/expected.tsx new file mode 100644 index 0000000..53cb416 --- /dev/null +++ b/codemods/remove-forward-ref/tests/arrow-function-expression/expected.tsx @@ -0,0 +1,8 @@ +const MyInput = ( + { + ref, + ...props + } +) => { + return null; +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/arrow-function-expression/input.tsx b/codemods/remove-forward-ref/tests/arrow-function-expression/input.tsx new file mode 100644 index 0000000..88af3bd --- /dev/null +++ b/codemods/remove-forward-ref/tests/arrow-function-expression/input.tsx @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyInput = forwardRef((props, ref) => { + return null; +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/arrow-function-expression/metrics.json b/codemods/remove-forward-ref/tests/arrow-function-expression/metrics.json new file mode 100644 index 0000000..0c57c24 --- /dev/null +++ b/codemods/remove-forward-ref/tests/arrow-function-expression/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/arrow-function-expression/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/callee-is-member-expression/expected.tsx b/codemods/remove-forward-ref/tests/callee-is-member-expression/expected.tsx new file mode 100644 index 0000000..05547f5 --- /dev/null +++ b/codemods/remove-forward-ref/tests/callee-is-member-expression/expected.tsx @@ -0,0 +1,10 @@ +import * as React1 from 'react'; + +const MyInput = ( + { + ref, + ...props + } +) => { + return null; +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/callee-is-member-expression/input.tsx b/codemods/remove-forward-ref/tests/callee-is-member-expression/input.tsx new file mode 100644 index 0000000..5f3f314 --- /dev/null +++ b/codemods/remove-forward-ref/tests/callee-is-member-expression/input.tsx @@ -0,0 +1,5 @@ +import * as React1 from 'react'; + +const MyInput = React1.forwardRef((props, ref) => { + return null; +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/callee-is-member-expression/metrics.json b/codemods/remove-forward-ref/tests/callee-is-member-expression/metrics.json new file mode 100644 index 0000000..4f8189b --- /dev/null +++ b/codemods/remove-forward-ref/tests/callee-is-member-expression/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/callee-is-member-expression/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/forward-ref-import-2/expected.tsx b/codemods/remove-forward-ref/tests/forward-ref-import-2/expected.tsx new file mode 100644 index 0000000..6c2fb56 --- /dev/null +++ b/codemods/remove-forward-ref/tests/forward-ref-import-2/expected.tsx @@ -0,0 +1,10 @@ +import { useState } from 'react'; + +const MyInput = function MyInput( + { + ref, + ...props + } +) { + return null; +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/forward-ref-import-2/input.tsx b/codemods/remove-forward-ref/tests/forward-ref-import-2/input.tsx new file mode 100644 index 0000000..e6675dc --- /dev/null +++ b/codemods/remove-forward-ref/tests/forward-ref-import-2/input.tsx @@ -0,0 +1,5 @@ +import { forwardRef, useState } from 'react'; + +const MyInput = forwardRef(function MyInput(props, ref) { + return null; +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/forward-ref-import-2/metrics.json b/codemods/remove-forward-ref/tests/forward-ref-import-2/metrics.json new file mode 100644 index 0000000..fede801 --- /dev/null +++ b/codemods/remove-forward-ref/tests/forward-ref-import-2/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/forward-ref-import-2/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/forward-ref-import/expected.tsx b/codemods/remove-forward-ref/tests/forward-ref-import/expected.tsx new file mode 100644 index 0000000..033a974 --- /dev/null +++ b/codemods/remove-forward-ref/tests/forward-ref-import/expected.tsx @@ -0,0 +1,8 @@ +const MyInput = function MyInput( + { + ref, + ...props + } +) { + return null; +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/forward-ref-import/input.tsx b/codemods/remove-forward-ref/tests/forward-ref-import/input.tsx new file mode 100644 index 0000000..6623c96 --- /dev/null +++ b/codemods/remove-forward-ref/tests/forward-ref-import/input.tsx @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyInput = forwardRef(function MyInput(props, ref) { + return null; +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/forward-ref-import/metrics.json b/codemods/remove-forward-ref/tests/forward-ref-import/metrics.json new file mode 100644 index 0000000..24e45e5 --- /dev/null +++ b/codemods/remove-forward-ref/tests/forward-ref-import/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/forward-ref-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/function-expression/expected.tsx b/codemods/remove-forward-ref/tests/function-expression/expected.tsx new file mode 100644 index 0000000..cf65821 --- /dev/null +++ b/codemods/remove-forward-ref/tests/function-expression/expected.tsx @@ -0,0 +1,8 @@ +const MyInput = function A( + { + ref, + ...props + } +) { + return null; +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/function-expression/input.tsx b/codemods/remove-forward-ref/tests/function-expression/input.tsx new file mode 100644 index 0000000..0466217 --- /dev/null +++ b/codemods/remove-forward-ref/tests/function-expression/input.tsx @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyInput = forwardRef(function A(props, ref) { + return null; +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/function-expression/metrics.json b/codemods/remove-forward-ref/tests/function-expression/metrics.json new file mode 100644 index 0000000..8e42ed8 --- /dev/null +++ b/codemods/remove-forward-ref/tests/function-expression/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/function-expression/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/props-identifier/expected.tsx b/codemods/remove-forward-ref/tests/props-identifier/expected.tsx new file mode 100644 index 0000000..297637a --- /dev/null +++ b/codemods/remove-forward-ref/tests/props-identifier/expected.tsx @@ -0,0 +1,8 @@ +const MyInput = function MyInput( + { + ref, + ...props + } +) { + return +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/props-identifier/input.tsx b/codemods/remove-forward-ref/tests/props-identifier/input.tsx new file mode 100644 index 0000000..bb87e8c --- /dev/null +++ b/codemods/remove-forward-ref/tests/props-identifier/input.tsx @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyInput = forwardRef(function MyInput(props, ref) { + return +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/props-identifier/metrics.json b/codemods/remove-forward-ref/tests/props-identifier/metrics.json new file mode 100644 index 0000000..31971f0 --- /dev/null +++ b/codemods/remove-forward-ref/tests/props-identifier/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/props-identifier/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/props-object-pattern/expected.tsx b/codemods/remove-forward-ref/tests/props-object-pattern/expected.tsx new file mode 100644 index 0000000..651d524 --- /dev/null +++ b/codemods/remove-forward-ref/tests/props-object-pattern/expected.tsx @@ -0,0 +1,8 @@ +const MyInput = function MyInput( + { + ref, + onChange + } +) { + return +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/props-object-pattern/input.tsx b/codemods/remove-forward-ref/tests/props-object-pattern/input.tsx new file mode 100644 index 0000000..99b9510 --- /dev/null +++ b/codemods/remove-forward-ref/tests/props-object-pattern/input.tsx @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyInput = forwardRef(function MyInput({ onChange }, ref) { + return +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/props-object-pattern/metrics.json b/codemods/remove-forward-ref/tests/props-object-pattern/metrics.json new file mode 100644 index 0000000..c94ce24 --- /dev/null +++ b/codemods/remove-forward-ref/tests/props-object-pattern/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/props-object-pattern/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-props-type-literal/expected.tsx b/codemods/remove-forward-ref/tests/typescript-props-type-literal/expected.tsx new file mode 100644 index 0000000..56fa6f5 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-props-type-literal/expected.tsx @@ -0,0 +1,10 @@ +const MyComponent = function Component( + { + ref: myRef, + ...myProps + }: { a: 1 } & { + ref: React.RefObject + } +) { + return null; +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-props-type-literal/input.tsx b/codemods/remove-forward-ref/tests/typescript-props-type-literal/input.tsx new file mode 100644 index 0000000..c242b87 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-props-type-literal/input.tsx @@ -0,0 +1,7 @@ +import { forwardRef } from 'react'; +const MyComponent = forwardRef(function Component( + myProps: { a: 1 }, + myRef +) { + return null; +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-props-type-literal/metrics.json b/codemods/remove-forward-ref/tests/typescript-props-type-literal/metrics.json new file mode 100644 index 0000000..7d4d936 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-props-type-literal/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/typescript-props-type-literal/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/expected.tsx b/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/expected.tsx new file mode 100644 index 0000000..e868776 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/expected.tsx @@ -0,0 +1,10 @@ +const MyComponent = function Component( + { + ref: myRef, + ...myProps + }: Props & { + ref: React.RefObject + } +) { + return null; +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/input.tsx b/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/input.tsx new file mode 100644 index 0000000..fd523ba --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/input.tsx @@ -0,0 +1,8 @@ +import { forwardRef } from 'react'; + +const MyComponent = forwardRef(function Component( + myProps: Props, + myRef: React.ForwardedRef +) { + return null; +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/metrics.json b/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/metrics.json new file mode 100644 index 0000000..11fa3f9 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/typescript-type-arguments-custom-names/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/expected.tsx b/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/expected.tsx new file mode 100644 index 0000000..f569dfc --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/expected.tsx @@ -0,0 +1,10 @@ +const MyInput = ( + { + ref, + ...props + }: { a: string } & { + ref: React.RefObject + } +) => { + return null; +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/input.tsx b/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/input.tsx new file mode 100644 index 0000000..f4f4ac0 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/input.tsx @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyInput = forwardRef((props, ref) => { + return null; +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/metrics.json b/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/metrics.json new file mode 100644 index 0000000..2e70083 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/typescript-type-arguments-type-literals/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments/expected.tsx b/codemods/remove-forward-ref/tests/typescript-type-arguments/expected.tsx new file mode 100644 index 0000000..23e1c16 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-type-arguments/expected.tsx @@ -0,0 +1,12 @@ +type Props = { a: 1 }; + +const MyInput = ( + { + ref, + ...props + }: Props & { + ref: React.RefObject + } +) => { + return null; +}; \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments/input.tsx b/codemods/remove-forward-ref/tests/typescript-type-arguments/input.tsx new file mode 100644 index 0000000..2a045b6 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-type-arguments/input.tsx @@ -0,0 +1,6 @@ +import { forwardRef } from 'react'; +type Props = { a: 1 }; + +const MyInput = forwardRef((props, ref) => { + return null; +}); \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments/metrics.json b/codemods/remove-forward-ref/tests/typescript-type-arguments/metrics.json new file mode 100644 index 0000000..2ce2d16 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-type-arguments/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/typescript-type-arguments/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tsconfig.json b/codemods/remove-forward-ref/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/remove-forward-ref/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/codemods/remove-forward-ref/workflow.yaml b/codemods/remove-forward-ref/workflow.yaml new file mode 100644 index 0000000..8773841 --- /dev/null +++ b/codemods/remove-forward-ref/workflow.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Scan tsx files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + language: "tsx" diff --git a/package.json b/package.json index f534f28..635d795 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,14 @@ "test:find-dom-node": "pnpm --filter @react/find-dom-node test", "test:react-proptypes-to-prop-types": "pnpm --filter @react/react-proptypes-to-prop-types test", "test:pure-render-mixin": "pnpm --filter @react/pure-render-mixin test", + "test:pure-component": "pnpm --filter @react/pure-component test", "test:sort-comp": "pnpm --filter @react/sort-comp test", "test:manual-bind-to-arrow": "pnpm --filter @react/manual-bind-to-arrow test", "test:create-element-to-jsx": "pnpm --filter @react/create-element-to-jsx test", + "test:remove-context-provider": "pnpm --filter @react/remove-context-provider test", + "test:remove-forward-ref": "pnpm --filter @react/remove-forward-ref test", + "test:react-dom-to-react-dom-factories": "pnpm --filter @react/react-dom-to-react-dom-factories test", + "test:react-native-view-prop-types": "pnpm --filter @react/react-native-view-prop-types test", "check-types": "pnpm -r check-types" }, "keywords": [ From 47686206193700365c0d388d880fdfdb019bd56e Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Fri, 10 Apr 2026 20:53:42 +0200 Subject: [PATCH 04/29] port react-to-react-dom --- codemods/react-to-react-dom/README.md | 16 + codemods/react-to-react-dom/codemod.yaml | 19 + codemods/react-to-react-dom/package.json | 15 + .../react-to-react-dom/scripts/codemod.ts | 713 ++++++++++++++++++ .../scripts/error-tests.mjs | 82 ++ .../import-dom-base-uppercase/expected.tsx | 4 + .../tests/import-dom-base-uppercase/input.tsx | 4 + .../import-dom-base-uppercase/metrics.json | 12 + .../tests/import-dom-base/expected.tsx | 8 + .../tests/import-dom-base/input.tsx | 8 + .../tests/import-dom-base/metrics.json | 12 + .../import-dom-type-identifier/expected.tsx | 5 + .../import-dom-type-identifier/input.tsx | 5 + .../import-dom-type-identifier/metrics.json | 12 + .../import-multiple-specifiers/expected.tsx | 7 + .../import-multiple-specifiers/input.tsx | 5 + .../import-multiple-specifiers/metrics.json | 12 + .../tests/import-server-base/expected.tsx | 4 + .../tests/import-server-base/input.tsx | 4 + .../tests/import-server-base/metrics.json | 12 + .../expected.tsx | 6 + .../import-with-existing-react-dom/input.tsx | 5 + .../metrics.json | 12 + .../expected.tsx | 3 + .../input.tsx | 3 + .../metrics.json | 12 + .../expected.tsx | 2 + .../mixed-with-existing-react-dom/input.tsx | 3 + .../metrics.json | 12 + .../require-dom-base-uppercase/expected.tsx | 4 + .../require-dom-base-uppercase/input.tsx | 4 + .../require-dom-base-uppercase/metrics.json | 12 + .../tests/require-dom-base/expected.tsx | 8 + .../tests/require-dom-base/input.tsx | 8 + .../tests/require-dom-base/metrics.json | 12 + .../tests/require-indirect-mixed/expected.tsx | 12 + .../tests/require-indirect-mixed/input.tsx | 8 + .../tests/require-indirect-mixed/metrics.json | 12 + .../tests/require-indirect/expected.tsx | 11 + .../tests/require-indirect/input.tsx | 6 + .../tests/require-indirect/metrics.json | 12 + .../tests/require-keeps-react/expected.tsx | 7 + .../tests/require-keeps-react/input.tsx | 4 + .../tests/require-keeps-react/metrics.json | 12 + .../tests/require-server-base/expected.tsx | 4 + .../tests/require-server-base/input.tsx | 4 + .../tests/require-server-base/metrics.json | 12 + codemods/react-to-react-dom/tsconfig.json | 27 + codemods/react-to-react-dom/workflow.yaml | 13 + package.json | 1 + 50 files changed, 1210 insertions(+) create mode 100644 codemods/react-to-react-dom/README.md create mode 100644 codemods/react-to-react-dom/codemod.yaml create mode 100644 codemods/react-to-react-dom/package.json create mode 100644 codemods/react-to-react-dom/scripts/codemod.ts create mode 100644 codemods/react-to-react-dom/scripts/error-tests.mjs create mode 100644 codemods/react-to-react-dom/tests/import-dom-base-uppercase/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/import-dom-base-uppercase/input.tsx create mode 100644 codemods/react-to-react-dom/tests/import-dom-base-uppercase/metrics.json create mode 100644 codemods/react-to-react-dom/tests/import-dom-base/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/import-dom-base/input.tsx create mode 100644 codemods/react-to-react-dom/tests/import-dom-base/metrics.json create mode 100644 codemods/react-to-react-dom/tests/import-dom-type-identifier/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/import-dom-type-identifier/input.tsx create mode 100644 codemods/react-to-react-dom/tests/import-dom-type-identifier/metrics.json create mode 100644 codemods/react-to-react-dom/tests/import-multiple-specifiers/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/import-multiple-specifiers/input.tsx create mode 100644 codemods/react-to-react-dom/tests/import-multiple-specifiers/metrics.json create mode 100644 codemods/react-to-react-dom/tests/import-server-base/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/import-server-base/input.tsx create mode 100644 codemods/react-to-react-dom/tests/import-server-base/metrics.json create mode 100644 codemods/react-to-react-dom/tests/import-with-existing-react-dom/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/import-with-existing-react-dom/input.tsx create mode 100644 codemods/react-to-react-dom/tests/import-with-existing-react-dom/metrics.json create mode 100644 codemods/react-to-react-dom/tests/import-without-default-specifier/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/import-without-default-specifier/input.tsx create mode 100644 codemods/react-to-react-dom/tests/import-without-default-specifier/metrics.json create mode 100644 codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/input.tsx create mode 100644 codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/metrics.json create mode 100644 codemods/react-to-react-dom/tests/require-dom-base-uppercase/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/require-dom-base-uppercase/input.tsx create mode 100644 codemods/react-to-react-dom/tests/require-dom-base-uppercase/metrics.json create mode 100644 codemods/react-to-react-dom/tests/require-dom-base/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/require-dom-base/input.tsx create mode 100644 codemods/react-to-react-dom/tests/require-dom-base/metrics.json create mode 100644 codemods/react-to-react-dom/tests/require-indirect-mixed/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/require-indirect-mixed/input.tsx create mode 100644 codemods/react-to-react-dom/tests/require-indirect-mixed/metrics.json create mode 100644 codemods/react-to-react-dom/tests/require-indirect/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/require-indirect/input.tsx create mode 100644 codemods/react-to-react-dom/tests/require-indirect/metrics.json create mode 100644 codemods/react-to-react-dom/tests/require-keeps-react/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/require-keeps-react/input.tsx create mode 100644 codemods/react-to-react-dom/tests/require-keeps-react/metrics.json create mode 100644 codemods/react-to-react-dom/tests/require-server-base/expected.tsx create mode 100644 codemods/react-to-react-dom/tests/require-server-base/input.tsx create mode 100644 codemods/react-to-react-dom/tests/require-server-base/metrics.json create mode 100644 codemods/react-to-react-dom/tsconfig.json create mode 100644 codemods/react-to-react-dom/workflow.yaml diff --git a/codemods/react-to-react-dom/README.md b/codemods/react-to-react-dom/README.md new file mode 100644 index 0000000..94db7a8 --- /dev/null +++ b/codemods/react-to-react-dom/README.md @@ -0,0 +1,16 @@ +# @react/react-to-react-dom + +Move DOM APIs from `react` to `react-dom` and `react-dom/server`. + +## Usage + +```bash +codemod run @react/react-to-react-dom +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/react-to-react-dom/codemod.yaml b/codemods/react-to-react-dom/codemod.yaml new file mode 100644 index 0000000..7d096f6 --- /dev/null +++ b/codemods/react-to-react-dom/codemod.yaml @@ -0,0 +1,19 @@ +schema_version: "1.0" + +name: "@react/react-to-react-dom" +version: "0.1.0" +description: "Move DOM APIs from react to react-dom" +author: "Codemod " +license: "MIT" +workflow: "workflow.yaml" + +targets: + languages: ["tsx"] + +keywords: ["React", "ReactDOM"] + +registry: + access: "public" + visibility: "public" + +capabilities: [] diff --git a/codemods/react-to-react-dom/package.json b/codemods/react-to-react-dom/package.json new file mode 100644 index 0000000..e888dc4 --- /dev/null +++ b/codemods/react-to-react-dom/package.json @@ -0,0 +1,15 @@ +{ + "name": "@react/react-to-react-dom", + "version": "0.1.0", + "description": "Move DOM APIs from react to react-dom", + "type": "module", + "scripts": { + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose && node --test ./scripts/error-tests.mjs", + "check-types": "tsc --noEmit" + }, + "devDependencies": { + "@codemod.com/jssg-types": "latest", + "@types/node": "latest", + "typescript": "latest" + } +} diff --git a/codemods/react-to-react-dom/scripts/codemod.ts b/codemods/react-to-react-dom/scripts/codemod.ts new file mode 100644 index 0000000..102ece7 --- /dev/null +++ b/codemods/react-to-react-dom/scripts/codemod.ts @@ -0,0 +1,713 @@ +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; +import type TSX from "codemod:ast-grep/langs/tsx"; +import { useMetricAtom } from "codemod:metrics"; + +type NamedSpecifier = { + name: string; + alias?: string; +}; + +type ImportBinding = { + alias: string | null; + named: NamedSpecifier[]; + statements: SgNode[]; + quote: string; +}; + +type RequireBinding = { + alias: string; + statement: SgNode; + kind: "var" | "let" | "const"; +}; + +type AssignmentBinding = { + alias: string; + declaration: SgNode | null; + assignment: SgNode; + kind: "var" | "let" | "const"; +}; + +type ModulePair = { + core: string; + dom: string; + server: string; + domAlias: string; + serverAlias: string; +}; + +type MemberAnalysis = { + edits: Edit[]; + domUses: number; + serverUses: number; + coreUses: number; +}; + +type DestructureAnalysis = { + edits: Edit[]; + domUses: number; + coreUses: number; +}; + +const CORE_PROPERTIES = [ + "Children", + "Component", + "createElement", + "cloneElement", + "isValidElement", + "PropTypes", + "createClass", + "createFactory", + "createMixin", + "DOM", + "__spread", +] as const; + +const DOM_PROPERTIES = [ + "findDOMNode", + "render", + "unmountComponentAtNode", + "unstable_batchedUpdates", + "unstable_renderSubtreeIntoContainer", +] as const; + +const DOM_SERVER_PROPERTIES = [ + "renderToString", + "renderToStaticMarkup", +] as const; + +const CORE_SET = new Set(CORE_PROPERTIES); +const DOM_SET = new Set(DOM_PROPERTIES); +const DOM_SERVER_SET = new Set(DOM_SERVER_PROPERTIES); + +const MODULE_PAIRS: ModulePair[] = [ + { + core: "React", + dom: "ReactDOM", + server: "ReactDOMServer", + domAlias: "ReactDOM", + serverAlias: "ReactDOMServer", + }, + { + core: "react", + dom: "react-dom", + server: "react-dom/server", + domAlias: "ReactDOM", + serverAlias: "ReactDOMServer", + }, +]; + +function metricFile(filename: string): string { + const cwd = process.cwd() + "/"; + return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; +} + +function namedChildren(node: SgNode | null): SgNode[] { + if (!node) { + return []; + } + + return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function exactRule(kind: "identifier" | "type_identifier" | "property_identifier", name: string) { + return { + kind, + regex: `^${name}$`, + } as const; +} + +function statementRange(node: SgNode, source: string): { start: number; end: number } { + const range = node.range(); + let end = range.end.index; + while (end < source.length && (source[end] === "\n" || source[end] === "\r")) { + end++; + } + return { start: range.start.index, end }; +} + +function sourceText(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) { + return fragment.text(); + } + + const text = node.text(); + if (text.length >= 2) { + return text.slice(1, -1); + } + + return null; +} + +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? sourceText(source) : null; +} + +function quoteForImport(node: SgNode): string { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + const text = source?.text() ?? "'react'"; + return text.startsWith("\"") ? "\"" : "'"; +} + +function callArguments(call: SgNode): SgNode[] { + return namedChildren(call.field("arguments")); +} + +function isRequireOf(node: SgNode | null, moduleName: string): boolean { + if (!node || node.kind() !== "call_expression") { + return false; + } + + const callee = node.field("function"); + const firstArg = callArguments(node)[0]; + return callee?.kind() === "identifier" && + callee.text() === "require" && + firstArg?.kind() === "string" && + sourceText(firstArg) === moduleName; +} + +function declarationKind(statement: SgNode): "var" | "let" | "const" { + const text = statement.text(); + if (text.startsWith("const ")) return "const"; + if (text.startsWith("let ")) return "let"; + return "var"; +} + +function buildImportLine( + quote: string, + source: string, + defaultAlias: string | null, + named: NamedSpecifier[], +): string { + const parts: string[] = []; + if (defaultAlias) { + parts.push(defaultAlias); + } + if (named.length > 0) { + const rendered = named.map((spec) => + spec.alias && spec.alias !== spec.name ? `${spec.name} as ${spec.alias}` : spec.name + ); + parts.push(`{ ${rendered.join(", ")} }`); + } + return `import ${parts.join(", ")} from ${quote}${source}${quote};`; +} + +function buildRequireLine(kind: "var" | "let" | "const", alias: string, source: string): string { + return `${kind} ${alias} = require('${source}');`; +} + +function buildRequireDeclaration(kind: "var" | "let" | "const", alias: string): string { + return `${kind} ${alias};`; +} + +function buildRequireAssignment(alias: string, source: string): string { + return `${alias} = require('${source}');`; +} + +function uniqueSpecs(specs: NamedSpecifier[]): NamedSpecifier[] { + const seen = new Set(); + const result: NamedSpecifier[] = []; + for (const spec of specs) { + const key = `${spec.name}:${spec.alias ?? ""}`; + if (!seen.has(key)) { + seen.add(key); + result.push(spec); + } + } + return result; +} + +function sortSpecs(specs: NamedSpecifier[]): NamedSpecifier[] { + return [...specs].sort((left, right) => left.name.localeCompare(right.name)); +} + +function findModuleImports(rootNode: SgNode, moduleName: string): ImportBinding { + const statements = rootNode.findAll({ rule: { kind: "import_statement" } }) + .filter((stmt) => importSource(stmt) === moduleName); + + const named: NamedSpecifier[] = []; + let alias: string | null = null; + let quote = "'"; + + for (const stmt of statements) { + quote = quoteForImport(stmt); + const clause = stmt.find({ rule: { kind: "import_clause" } }); + if (!clause) { + continue; + } + + const defaultIdentifier = namedChildren(clause) + .find((child) => child.kind() === "identifier"); + if (defaultIdentifier) { + alias = defaultIdentifier.text(); + } + + for (const spec of stmt.findAll({ rule: { kind: "import_specifier" } })) { + named.push({ + name: spec.field("name")?.text() ?? spec.text(), + alias: spec.field("alias")?.text() ?? undefined, + }); + } + } + + return { + alias, + named: uniqueSpecs(named), + statements, + quote, + }; +} + +function findRequireBinding(rootNode: SgNode, moduleName: string): RequireBinding | null { + for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { + const name = declarator.field("name"); + const value = declarator.field("value"); + if (name?.kind() !== "identifier" || !isRequireOf(value, moduleName)) { + continue; + } + + const statement = declarator.ancestors().find((ancestor) => + ancestor.kind() === "variable_declaration" || ancestor.kind() === "lexical_declaration" + ); + if (!statement) { + continue; + } + + return { + alias: name.text(), + statement, + kind: declarationKind(statement), + }; + } + + return null; +} + +function findAssignmentBinding(rootNode: SgNode, moduleName: string): AssignmentBinding | null { + for (const assignment of rootNode.findAll({ rule: { kind: "assignment_expression" } })) { + const left = assignment.field("left"); + const right = assignment.field("right"); + if (left?.kind() !== "identifier" || !isRequireOf(right, moduleName)) { + continue; + } + + const assignmentStatement = assignment.ancestors().find((ancestor) => ancestor.kind() === "expression_statement"); + if (!assignmentStatement) { + continue; + } + + const alias = left.text(); + const bareDeclaration = rootNode.findAll({ rule: { kind: "variable_declarator" } }) + .find((declarator) => declarator.field("name")?.text() === alias && !declarator.field("value")); + const declaration = bareDeclaration?.ancestors().find((ancestor) => + ancestor.kind() === "variable_declaration" || ancestor.kind() === "lexical_declaration" + ) ?? null; + + return { + alias, + declaration, + assignment: assignmentStatement, + kind: declaration ? declarationKind(declaration) : "var", + }; + } + + return null; +} + +function hasCoreSource(rootNode: SgNode, moduleName: string): boolean { + return findModuleImports(rootNode, moduleName).statements.length > 0 || + findRequireBinding(rootNode, moduleName) !== null || + findAssignmentBinding(rootNode, moduleName) !== null; +} + +function activePair(rootNode: SgNode): ModulePair | null { + for (const pair of MODULE_PAIRS) { + if (hasCoreSource(rootNode, pair.core)) { + return pair; + } + } + + return null; +} + +function hasJSX(rootNode: SgNode): boolean { + return rootNode.findAll({ + rule: { + any: [ + { kind: "jsx_element" }, + { kind: "jsx_self_closing_element" }, + ], + }, + }).length > 0; +} + +function isImportContext(node: SgNode): boolean { + return node.ancestors().some((ancestor) => ancestor.kind() === "import_statement"); +} + +function memberPropertyName(node: SgNode): string | null { + if (node.kind() === "member_expression") { + return node.field("property")?.text() ?? null; + } + + if (node.kind() === "nested_type_identifier") { + const children = namedChildren(node); + return children[1]?.text() ?? null; + } + + return null; +} + +function memberObjectNode(node: SgNode): SgNode | null { + if (node.kind() === "member_expression") { + return node.field("object"); + } + + if (node.kind() === "nested_type_identifier") { + return namedChildren(node)[0] ?? null; + } + + return null; +} + +function isIdentifierNode(node: SgNode | null, name: string): boolean { + return node?.kind() === "identifier" && node.text() === name; +} + +function classifyProperty(name: string): "core" | "dom" | "server" | "unknown" { + if (CORE_SET.has(name)) return "core"; + if (DOM_SET.has(name)) return "dom"; + if (DOM_SERVER_SET.has(name)) return "server"; + return "unknown"; +} + +function analyzeReactMembers( + rootNode: SgNode, + reactAlias: string, + domAlias: string, + serverAlias: string, +): MemberAnalysis { + const edits: Edit[] = []; + let domUses = 0; + let serverUses = 0; + let coreUses = 0; + + const nodes = rootNode.findAll({ + rule: { + any: [ + { kind: "member_expression" }, + { kind: "nested_type_identifier" }, + ], + }, + }); + + for (const node of nodes) { + if (node.kind() === "member_expression" && isImportContext(node)) { + continue; + } + + const object = memberObjectNode(node); + const propertyName = memberPropertyName(node); + if (!isIdentifierNode(object, reactAlias) || !propertyName) { + continue; + } + + const category = classifyProperty(propertyName); + if (category === "unknown") { + throw new Error(`Unknown property React.${propertyName}`); + } + + if (category === "core") { + coreUses++; + continue; + } + + edits.push(object.replace(category === "dom" ? domAlias : serverAlias)); + if (category === "dom") { + domUses++; + } else { + serverUses++; + } + } + + return { edits, domUses, serverUses, coreUses }; +} + +function destructuredNames(pattern: SgNode): string[] { + return pattern.findAll({ + rule: { + any: [ + { kind: "shorthand_property_identifier_pattern" }, + { kind: "pair_pattern" }, + ], + }, + }).map((property) => { + if (property.kind() === "shorthand_property_identifier_pattern") { + return property.text(); + } + return property.field("key")?.text() ?? property.text(); + }); +} + +function analyzeReactDestructures( + rootNode: SgNode, + reactAlias: string, + domAlias: string, +): DestructureAnalysis { + const edits: Edit[] = []; + let domUses = 0; + let coreUses = 0; + + for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { + const pattern = declarator.field("name"); + const value = declarator.field("value"); + if (pattern?.kind() !== "object_pattern" || !isIdentifierNode(value, reactAlias)) { + continue; + } + + const core: string[] = []; + const dom: string[] = []; + + for (const name of destructuredNames(pattern)) { + const category = classifyProperty(name); + if (category === "unknown" || category === "server") { + throw new Error(`Unknown property React.${name} while destructuring`); + } + if (category === "core") { + core.push(name); + } else { + dom.push(name); + } + } + + if (dom.length === 0) { + coreUses++; + continue; + } + + if (core.length === 0) { + edits.push(value.replace(domAlias)); + domUses++; + continue; + } + + const statement = declarator.ancestors().find((ancestor) => + ancestor.kind() === "variable_declaration" || ancestor.kind() === "lexical_declaration" + ); + if (!statement) { + continue; + } + + const declarators = namedChildren(statement).filter((child) => child.kind() === "variable_declarator"); + if (declarators.length !== 1) { + throw new Error("Mixed React destructuring with multiple declarators is unsupported"); + } + + const kind = declarationKind(statement); + edits.push(statement.replace( + `${kind} { ${core.join(", ")} } = ${reactAlias};\n${kind} { ${dom.join(", ")} } = ${domAlias};`, + )); + domUses++; + coreUses++; + } + + return { edits, domUses, coreUses }; +} + +function namedFromReact(binding: ImportBinding, names: readonly string[]): NamedSpecifier[] { + const wanted = new Set(names); + return binding.named.filter((spec) => wanted.has(spec.name)); +} + +function withoutNamed(binding: ImportBinding, names: readonly string[]): NamedSpecifier[] { + const removed = new Set(names); + return binding.named.filter((spec) => !removed.has(spec.name)); +} + +function hasIdentifierReference(rootNode: SgNode, name: string): boolean { + return rootNode.findAll({ + rule: { + any: [ + exactRule("identifier", name), + exactRule("type_identifier", name), + exactRule("property_identifier", name), + ], + }, + }).length > 0; +} + +const transform: Transform = async (root) => { + const rootNode = root.root(); + const source = rootNode.text(); + const metric = useMetricAtom("react-to-react-dom-migrations"); + const edits: Edit[] = []; + + const pair = activePair(rootNode); + if (!pair) { + return null; + } + + const reactImport = findModuleImports(rootNode, pair.core); + const reactDomImport = findModuleImports(rootNode, pair.dom); + const reactDomServerImport = findModuleImports(rootNode, pair.server); + const reactRequire = findRequireBinding(rootNode, pair.core); + const reactAssign = findAssignmentBinding(rootNode, pair.core); + const reactDomRequire = findRequireBinding(rootNode, pair.dom); + const reactDomServerRequire = findRequireBinding(rootNode, pair.server); + + const coreDeclarationCount = reactImport.statements.length + (reactRequire ? 1 : 0) + (reactAssign ? 1 : 0); + if (coreDeclarationCount === 0) { + return null; + } + if (coreDeclarationCount > 1) { + throw new Error("Multiple declarations of React"); + } + + const reactAlias = reactImport.alias ?? reactRequire?.alias ?? reactAssign?.alias ?? null; + const domAlias = reactDomImport.alias ?? reactDomRequire?.alias ?? pair.domAlias; + const serverAlias = reactDomServerImport.alias ?? reactDomServerRequire?.alias ?? pair.serverAlias; + + const memberAnalysis = reactAlias + ? analyzeReactMembers(rootNode, reactAlias, domAlias, serverAlias) + : { edits: [], domUses: 0, serverUses: 0, coreUses: 0 }; + const destructureAnalysis = reactAlias + ? analyzeReactDestructures(rootNode, reactAlias, domAlias) + : { edits: [], domUses: 0, coreUses: 0 }; + + const domNamedFromReact = namedFromReact(reactImport, DOM_PROPERTIES); + const serverNamedFromReact = namedFromReact(reactImport, DOM_SERVER_PROPERTIES); + const coreNamedFromReact = withoutNamed(reactImport, [...DOM_PROPERTIES, ...DOM_SERVER_PROPERTIES]); + + const needsDomBinding = + memberAnalysis.domUses > 0 || + destructureAnalysis.domUses > 0 || + domNamedFromReact.length > 0 || + reactDomImport.statements.length > 0; + const needsServerBinding = + memberAnalysis.serverUses > 0 || + serverNamedFromReact.length > 0 || + reactDomServerImport.statements.length > 0; + + if (needsDomBinding && !reactDomImport.alias && !reactDomRequire && hasIdentifierReference(rootNode, domAlias)) { + throw new Error("ReactDOM is already defined in a different scope than React"); + } + if (needsServerBinding && !reactDomServerImport.alias && !reactDomServerRequire && hasIdentifierReference(rootNode, serverAlias)) { + throw new Error("ReactDOMServer is already defined in a different scope than React"); + } + + edits.push(...memberAnalysis.edits, ...destructureAnalysis.edits); + + if (reactImport.statements.length > 0) { + const quote = reactImport.quote; + const keepReactDefault = reactImport.alias !== null && ( + hasJSX(rootNode) || + memberAnalysis.coreUses > 0 || + destructureAnalysis.coreUses > 0 + ); + + const nextReactNamed = sortSpecs(coreNamedFromReact); + const nextDomNamed = sortSpecs(uniqueSpecs([...reactDomImport.named, ...domNamedFromReact])); + const nextServerNamed = sortSpecs(uniqueSpecs([...reactDomServerImport.named, ...serverNamedFromReact])); + + const lines: string[] = []; + if (keepReactDefault || nextReactNamed.length > 0) { + lines.push(buildImportLine(quote, pair.core, keepReactDefault ? reactImport.alias : null, nextReactNamed)); + } + if (!reactDomRequire && needsDomBinding) { + const defaultAlias = reactDomImport.alias ?? (memberAnalysis.domUses > 0 || destructureAnalysis.domUses > 0 ? domAlias : null); + lines.push(buildImportLine(quote, pair.dom, defaultAlias, nextDomNamed)); + } + if (!reactDomServerRequire && needsServerBinding) { + const defaultAlias = reactDomServerImport.alias ?? (memberAnalysis.serverUses > 0 ? serverAlias : null); + lines.push(buildImportLine(quote, pair.server, defaultAlias, nextServerNamed)); + } + + const allImportStatements = [ + ...reactImport.statements, + ...reactDomImport.statements, + ...reactDomServerImport.statements, + ]; + const ranges = allImportStatements.map((stmt) => statementRange(stmt, source)); + const start = Math.min(...ranges.map((range) => range.start)); + const end = Math.max(...ranges.map((range) => range.end)); + edits.push({ + startPos: start, + endPos: end, + insertedText: lines.length > 0 ? `${lines.join("\n")}\n\n` : "", + }); + } + + if (reactRequire) { + const keepReact = hasJSX(rootNode) || memberAnalysis.coreUses > 0 || destructureAnalysis.coreUses > 0; + const inserted: string[] = []; + if (!reactDomRequire && (memberAnalysis.domUses > 0 || destructureAnalysis.domUses > 0)) { + inserted.push(buildRequireLine(reactRequire.kind, domAlias, pair.dom)); + } + if (!reactDomServerRequire && memberAnalysis.serverUses > 0) { + inserted.push(buildRequireLine(reactRequire.kind, serverAlias, pair.server)); + } + + const range = statementRange(reactRequire.statement, source); + if (keepReact) { + if (inserted.length > 0) { + edits.push({ + startPos: range.start, + endPos: range.end, + insertedText: `${reactRequire.statement.text()}\n\n${inserted.join("\n")}\n\n`, + }); + } + } else { + edits.push({ + startPos: range.start, + endPos: range.end, + insertedText: inserted.length > 0 ? `${inserted.join("\n")}\n` : "", + }); + } + } + + if (reactAssign) { + const declarationLines: string[] = []; + const assignmentLines: string[] = []; + + if (!reactDomRequire && (memberAnalysis.domUses > 0 || destructureAnalysis.domUses > 0)) { + declarationLines.push(buildRequireDeclaration(reactAssign.kind, domAlias)); + assignmentLines.push(buildRequireAssignment(domAlias, pair.dom)); + } + if (!reactDomServerRequire && memberAnalysis.serverUses > 0) { + declarationLines.push(buildRequireDeclaration(reactAssign.kind, serverAlias)); + assignmentLines.push(buildRequireAssignment(serverAlias, pair.server)); + } + + if (declarationLines.length > 0 && reactAssign.declaration) { + const declRange = statementRange(reactAssign.declaration, source); + edits.push({ + startPos: declRange.start, + endPos: declRange.end, + insertedText: `${reactAssign.declaration.text()}\n\n${declarationLines.join("\n")}\n\n`, + }); + } + if (assignmentLines.length > 0) { + const assignRange = statementRange(reactAssign.assignment, source); + edits.push({ + startPos: assignRange.start, + endPos: assignRange.end, + insertedText: `${reactAssign.assignment.text()}\n${assignmentLines.join("\n")}\n`, + }); + } + } + + if (edits.length === 0) { + return null; + } + + metric.increment({ + domMembers: String(memberAnalysis.domUses + destructureAnalysis.domUses + domNamedFromReact.length), + serverMembers: String(memberAnalysis.serverUses + serverNamedFromReact.length), + file: metricFile(root.filename()), + }); + + return rootNode.commitEdits(edits); +}; + +export default transform; diff --git a/codemods/react-to-react-dom/scripts/error-tests.mjs b/codemods/react-to-react-dom/scripts/error-tests.mjs new file mode 100644 index 0000000..118f9c7 --- /dev/null +++ b/codemods/react-to-react-dom/scripts/error-tests.mjs @@ -0,0 +1,82 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); +const packageDir = path.dirname(path.dirname(__filename)); +const workflowPath = path.join(packageDir, "workflow.yaml"); + +function runCodemod(input) { + const target = mkdtempSync(path.join(tmpdir(), "react-to-react-dom-")); + const filePath = path.join(target, "input.tsx"); + writeFileSync(filePath, input, "utf8"); + + try { + return spawnSync( + "pnpm", + [ + "dlx", + "codemod@latest", + "workflow", + "run", + "-w", + workflowPath, + "--target", + target, + "--allow-dirty", + ], + { + cwd: packageDir, + encoding: "utf8", + }, + ); + } finally { + rmSync(target, { recursive: true, force: true }); + } +} + +function expectFailure(input, pattern) { + const result = runCodemod(input); + const combined = `${result.stdout}\n${result.stderr}`; + assert.match(combined, pattern); + assert.match(combined, /Failed to execute codemod|InitializationFailed/); +} + +test("throws on conflicting ReactDOM binding", () => { + expectFailure( + "const ReactDOM = 1;\nimport React from 'react';\nReact.render(null);\n", + /ReactDOM is already defined in a different scope than React/, + ); +}); + +test("throws on conflicting ReactDOMServer binding", () => { + expectFailure( + "const ReactDOMServer = 1;\nimport React from 'react';\nReact.renderToString(null);\n", + /ReactDOMServer is already defined in a different scope than React/, + ); +}); + +test("throws on multiple React declarations", () => { + expectFailure( + "import React from 'react';\nimport Foo from 'react';\nReact.render(null);\n", + /Multiple declarations of React/, + ); +}); + +test("throws on unknown React member", () => { + expectFailure( + "import React from 'react';\nconst x = React.Fragment;\n", + /Unknown property React\.Fragment/, + ); +}); + +test("throws on unknown React destructuring", () => { + expectFailure( + "var React;\nReact = require('react');\nlet { Fragment } = React;\n", + /Unknown property React\.Fragment while destructuring/, + ); +}); diff --git a/codemods/react-to-react-dom/tests/import-dom-base-uppercase/expected.tsx b/codemods/react-to-react-dom/tests/import-dom-base-uppercase/expected.tsx new file mode 100644 index 0000000..8ece2af --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-dom-base-uppercase/expected.tsx @@ -0,0 +1,4 @@ +import ReactDOM from 'ReactDOM'; + +ReactDOM.findDOMNode(); +ReactDOM.render(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-base-uppercase/input.tsx b/codemods/react-to-react-dom/tests/import-dom-base-uppercase/input.tsx new file mode 100644 index 0000000..ae38804 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-dom-base-uppercase/input.tsx @@ -0,0 +1,4 @@ +import React from 'React'; + +React.findDOMNode(); +React.render(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-base-uppercase/metrics.json b/codemods/react-to-react-dom/tests/import-dom-base-uppercase/metrics.json new file mode 100644 index 0000000..be38b7d --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-dom-base-uppercase/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "2", + "file": "tests/import-dom-base-uppercase/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-dom-base/expected.tsx b/codemods/react-to-react-dom/tests/import-dom-base/expected.tsx new file mode 100644 index 0000000..87190bf --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-dom-base/expected.tsx @@ -0,0 +1,8 @@ +import ReactDOM from 'react-dom'; + +ReactDOM.findDOMNode(); +ReactDOM.findDOMNode(this.refs.node); +ReactDOM.render(null); +ReactDOM.unmountComponentAtNode(node); +ReactDOM.unstable_batchedUpdates(() => {}); +ReactDOM.unstable_renderSubtreeIntoContainer(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-base/input.tsx b/codemods/react-to-react-dom/tests/import-dom-base/input.tsx new file mode 100644 index 0000000..0191c3d --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-dom-base/input.tsx @@ -0,0 +1,8 @@ +import React from 'react'; + +React.findDOMNode(); +React.findDOMNode(this.refs.node); +React.render(null); +React.unmountComponentAtNode(node); +React.unstable_batchedUpdates(() => {}); +React.unstable_renderSubtreeIntoContainer(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-base/metrics.json b/codemods/react-to-react-dom/tests/import-dom-base/metrics.json new file mode 100644 index 0000000..a191839 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-dom-base/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "6", + "file": "tests/import-dom-base/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-dom-type-identifier/expected.tsx b/codemods/react-to-react-dom/tests/import-dom-type-identifier/expected.tsx new file mode 100644 index 0000000..dd1f21f --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-dom-type-identifier/expected.tsx @@ -0,0 +1,5 @@ +import ReactDOM from 'react-dom'; + +type RenderRef = ReactDOM.render; + +ReactDOM.render(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-type-identifier/input.tsx b/codemods/react-to-react-dom/tests/import-dom-type-identifier/input.tsx new file mode 100644 index 0000000..3decd23 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-dom-type-identifier/input.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +type RenderRef = React.render; + +React.render(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-type-identifier/metrics.json b/codemods/react-to-react-dom/tests/import-dom-type-identifier/metrics.json new file mode 100644 index 0000000..3818519 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-dom-type-identifier/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "2", + "file": "tests/import-dom-type-identifier/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-multiple-specifiers/expected.tsx b/codemods/react-to-react-dom/tests/import-multiple-specifiers/expected.tsx new file mode 100644 index 0000000..d68800f --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-multiple-specifiers/expected.tsx @@ -0,0 +1,7 @@ +import React, { PropTypes } from 'react'; + +import { render, findDOMNode } from 'react-dom'; + +render(
    ); +findDOMNode(this.refs.node); +PropTypes.number diff --git a/codemods/react-to-react-dom/tests/import-multiple-specifiers/input.tsx b/codemods/react-to-react-dom/tests/import-multiple-specifiers/input.tsx new file mode 100644 index 0000000..d6c950e --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-multiple-specifiers/input.tsx @@ -0,0 +1,5 @@ +import React, { PropTypes, render, findDOMNode } from 'react'; + +render(
    ); +findDOMNode(this.refs.node); +PropTypes.number diff --git a/codemods/react-to-react-dom/tests/import-multiple-specifiers/metrics.json b/codemods/react-to-react-dom/tests/import-multiple-specifiers/metrics.json new file mode 100644 index 0000000..afb685e --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-multiple-specifiers/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "2", + "file": "tests/import-multiple-specifiers/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-server-base/expected.tsx b/codemods/react-to-react-dom/tests/import-server-base/expected.tsx new file mode 100644 index 0000000..4369aad --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-server-base/expected.tsx @@ -0,0 +1,4 @@ +import ReactDOMServer from 'react-dom/server'; + +ReactDOMServer.renderToString(); +ReactDOMServer.renderToStaticMarkup(); diff --git a/codemods/react-to-react-dom/tests/import-server-base/input.tsx b/codemods/react-to-react-dom/tests/import-server-base/input.tsx new file mode 100644 index 0000000..4191bf4 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-server-base/input.tsx @@ -0,0 +1,4 @@ +import React from 'react'; + +React.renderToString(); +React.renderToStaticMarkup(); diff --git a/codemods/react-to-react-dom/tests/import-server-base/metrics.json b/codemods/react-to-react-dom/tests/import-server-base/metrics.json new file mode 100644 index 0000000..899310c --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-server-base/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "0", + "file": "tests/import-server-base/input.tsx", + "serverMembers": "2" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-with-existing-react-dom/expected.tsx b/codemods/react-to-react-dom/tests/import-with-existing-react-dom/expected.tsx new file mode 100644 index 0000000..30a3027 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-with-existing-react-dom/expected.tsx @@ -0,0 +1,6 @@ +import React, { PropTypes } from 'react'; +import ReactDOM, { findDOMNode } from 'react-dom'; + +ReactDOM.render(
    ); +findDOMNode(this.refs.node); + diff --git a/codemods/react-to-react-dom/tests/import-with-existing-react-dom/input.tsx b/codemods/react-to-react-dom/tests/import-with-existing-react-dom/input.tsx new file mode 100644 index 0000000..bcbe45f --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-with-existing-react-dom/input.tsx @@ -0,0 +1,5 @@ +import React, { PropTypes, findDOMNode } from 'react'; +import ReactDOM from 'react-dom'; + +React.render(
    ); +findDOMNode(this.refs.node); diff --git a/codemods/react-to-react-dom/tests/import-with-existing-react-dom/metrics.json b/codemods/react-to-react-dom/tests/import-with-existing-react-dom/metrics.json new file mode 100644 index 0000000..7767d53 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-with-existing-react-dom/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "2", + "file": "tests/import-with-existing-react-dom/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-without-default-specifier/expected.tsx b/codemods/react-to-react-dom/tests/import-without-default-specifier/expected.tsx new file mode 100644 index 0000000..95febc5 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-without-default-specifier/expected.tsx @@ -0,0 +1,3 @@ +import { findDOMNode } from 'react-dom'; + +findDOMNode(this.refs.node); diff --git a/codemods/react-to-react-dom/tests/import-without-default-specifier/input.tsx b/codemods/react-to-react-dom/tests/import-without-default-specifier/input.tsx new file mode 100644 index 0000000..33c92e0 --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-without-default-specifier/input.tsx @@ -0,0 +1,3 @@ +import { findDOMNode } from 'react'; + +findDOMNode(this.refs.node); diff --git a/codemods/react-to-react-dom/tests/import-without-default-specifier/metrics.json b/codemods/react-to-react-dom/tests/import-without-default-specifier/metrics.json new file mode 100644 index 0000000..289eccc --- /dev/null +++ b/codemods/react-to-react-dom/tests/import-without-default-specifier/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "1", + "file": "tests/import-without-default-specifier/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/expected.tsx b/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/expected.tsx new file mode 100644 index 0000000..efd1e01 --- /dev/null +++ b/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/expected.tsx @@ -0,0 +1,2 @@ +var ReactDOM = require('react-dom'); +ReactDOM.findDOMNode(); diff --git a/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/input.tsx b/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/input.tsx new file mode 100644 index 0000000..fd563e9 --- /dev/null +++ b/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/input.tsx @@ -0,0 +1,3 @@ +import React from 'react'; +var ReactDOM = require('react-dom'); +React.findDOMNode(); diff --git a/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/metrics.json b/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/metrics.json new file mode 100644 index 0000000..e903a51 --- /dev/null +++ b/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "1", + "file": "tests/mixed-with-existing-react-dom/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-dom-base-uppercase/expected.tsx b/codemods/react-to-react-dom/tests/require-dom-base-uppercase/expected.tsx new file mode 100644 index 0000000..635b207 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-dom-base-uppercase/expected.tsx @@ -0,0 +1,4 @@ +var ReactDOM = require('ReactDOM'); + +ReactDOM.findDOMNode(); +ReactDOM.render(null); diff --git a/codemods/react-to-react-dom/tests/require-dom-base-uppercase/input.tsx b/codemods/react-to-react-dom/tests/require-dom-base-uppercase/input.tsx new file mode 100644 index 0000000..7f949f8 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-dom-base-uppercase/input.tsx @@ -0,0 +1,4 @@ +var React = require('React'); + +React.findDOMNode(); +React.render(null); diff --git a/codemods/react-to-react-dom/tests/require-dom-base-uppercase/metrics.json b/codemods/react-to-react-dom/tests/require-dom-base-uppercase/metrics.json new file mode 100644 index 0000000..c46bb29 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-dom-base-uppercase/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "2", + "file": "tests/require-dom-base-uppercase/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-dom-base/expected.tsx b/codemods/react-to-react-dom/tests/require-dom-base/expected.tsx new file mode 100644 index 0000000..8ae07bc --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-dom-base/expected.tsx @@ -0,0 +1,8 @@ +var ReactDOM = require('react-dom'); + +ReactDOM.findDOMNode(); +ReactDOM.findDOMNode(this.refs.node); +ReactDOM.render(null); +ReactDOM.unmountComponentAtNode(node); +ReactDOM.unstable_batchedUpdates(() => {}); +ReactDOM.unstable_renderSubtreeIntoContainer(null); diff --git a/codemods/react-to-react-dom/tests/require-dom-base/input.tsx b/codemods/react-to-react-dom/tests/require-dom-base/input.tsx new file mode 100644 index 0000000..cf3fe29 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-dom-base/input.tsx @@ -0,0 +1,8 @@ +var React = require('react'); + +React.findDOMNode(); +React.findDOMNode(this.refs.node); +React.render(null); +React.unmountComponentAtNode(node); +React.unstable_batchedUpdates(() => {}); +React.unstable_renderSubtreeIntoContainer(null); diff --git a/codemods/react-to-react-dom/tests/require-dom-base/metrics.json b/codemods/react-to-react-dom/tests/require-dom-base/metrics.json new file mode 100644 index 0000000..b8c1387 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-dom-base/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "6", + "file": "tests/require-dom-base/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-indirect-mixed/expected.tsx b/codemods/react-to-react-dom/tests/require-indirect-mixed/expected.tsx new file mode 100644 index 0000000..c7e5c74 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-indirect-mixed/expected.tsx @@ -0,0 +1,12 @@ +var React; + +var ReactDOM; + +React = require('react'); +ReactDOM = require('react-dom'); +let { Component } = React; +let { render } = ReactDOM; + +class Foo extends Component {} + +render(
    ); diff --git a/codemods/react-to-react-dom/tests/require-indirect-mixed/input.tsx b/codemods/react-to-react-dom/tests/require-indirect-mixed/input.tsx new file mode 100644 index 0000000..7180a6a --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-indirect-mixed/input.tsx @@ -0,0 +1,8 @@ +var React; + +React = require('react'); +let { Component, render } = React; + +class Foo extends Component {} + +render(
    ); diff --git a/codemods/react-to-react-dom/tests/require-indirect-mixed/metrics.json b/codemods/react-to-react-dom/tests/require-indirect-mixed/metrics.json new file mode 100644 index 0000000..de32ab7 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-indirect-mixed/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "1", + "file": "tests/require-indirect-mixed/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-indirect/expected.tsx b/codemods/react-to-react-dom/tests/require-indirect/expected.tsx new file mode 100644 index 0000000..fcbf2e9 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-indirect/expected.tsx @@ -0,0 +1,11 @@ +var React; + +var ReactDOM; + +React = require('react'); +ReactDOM = require('react-dom'); +let { + render +} = ReactDOM; + +render(
    ); diff --git a/codemods/react-to-react-dom/tests/require-indirect/input.tsx b/codemods/react-to-react-dom/tests/require-indirect/input.tsx new file mode 100644 index 0000000..de1f202 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-indirect/input.tsx @@ -0,0 +1,6 @@ +var React; + +React = require('react'); +let { render } = React; + +render(
    ); diff --git a/codemods/react-to-react-dom/tests/require-indirect/metrics.json b/codemods/react-to-react-dom/tests/require-indirect/metrics.json new file mode 100644 index 0000000..b9d4a53 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-indirect/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "1", + "file": "tests/require-indirect/input.tsx", + "serverMembers": "0" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-keeps-react/expected.tsx b/codemods/react-to-react-dom/tests/require-keeps-react/expected.tsx new file mode 100644 index 0000000..0f35a74 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-keeps-react/expected.tsx @@ -0,0 +1,7 @@ +var React = require('react'); + +var ReactDOM = require('react-dom'); +var ReactDOMServer = require('react-dom/server'); + +ReactDOM.render(
    ); +ReactDOMServer.renderToString(); diff --git a/codemods/react-to-react-dom/tests/require-keeps-react/input.tsx b/codemods/react-to-react-dom/tests/require-keeps-react/input.tsx new file mode 100644 index 0000000..c756d06 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-keeps-react/input.tsx @@ -0,0 +1,4 @@ +var React = require('react'); + +React.render(
    ); +React.renderToString(); diff --git a/codemods/react-to-react-dom/tests/require-keeps-react/metrics.json b/codemods/react-to-react-dom/tests/require-keeps-react/metrics.json new file mode 100644 index 0000000..397e979 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-keeps-react/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "1", + "file": "tests/require-keeps-react/input.tsx", + "serverMembers": "1" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-server-base/expected.tsx b/codemods/react-to-react-dom/tests/require-server-base/expected.tsx new file mode 100644 index 0000000..63f42ae --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-server-base/expected.tsx @@ -0,0 +1,4 @@ +var ReactDOMServer = require('react-dom/server'); + +ReactDOMServer.renderToString(); +ReactDOMServer.renderToStaticMarkup(); diff --git a/codemods/react-to-react-dom/tests/require-server-base/input.tsx b/codemods/react-to-react-dom/tests/require-server-base/input.tsx new file mode 100644 index 0000000..6b7ded6 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-server-base/input.tsx @@ -0,0 +1,4 @@ +var React = require('react'); + +React.renderToString(); +React.renderToStaticMarkup(); diff --git a/codemods/react-to-react-dom/tests/require-server-base/metrics.json b/codemods/react-to-react-dom/tests/require-server-base/metrics.json new file mode 100644 index 0000000..8ad5612 --- /dev/null +++ b/codemods/react-to-react-dom/tests/require-server-base/metrics.json @@ -0,0 +1,12 @@ +{ + "react-to-react-dom-migrations": [ + { + "cardinality": { + "domMembers": "0", + "file": "tests/require-server-base/input.tsx", + "serverMembers": "2" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tsconfig.json b/codemods/react-to-react-dom/tsconfig.json new file mode 100644 index 0000000..decec57 --- /dev/null +++ b/codemods/react-to-react-dom/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": [ + "ESNext" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "types": [ + "@codemod.com/jssg-types", + "node" + ], + "allowImportingTsExtensions": true, + "noEmit": true, + "skipLibCheck": true, + "verbatimModuleSyntax": false, + "erasableSyntaxOnly": true, + "strict": true, + "strictNullChecks": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "tests" + ] +} diff --git a/codemods/react-to-react-dom/workflow.yaml b/codemods/react-to-react-dom/workflow.yaml new file mode 100644 index 0000000..8773841 --- /dev/null +++ b/codemods/react-to-react-dom/workflow.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Scan tsx files and apply fixes" + js-ast-grep: + js_file: scripts/codemod.ts + language: "tsx" diff --git a/package.json b/package.json index 635d795..d721c64 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "test:remove-forward-ref": "pnpm --filter @react/remove-forward-ref test", "test:react-dom-to-react-dom-factories": "pnpm --filter @react/react-dom-to-react-dom-factories test", "test:react-native-view-prop-types": "pnpm --filter @react/react-native-view-prop-types test", + "test:react-to-react-dom": "pnpm --filter @react/react-to-react-dom test", "check-types": "pnpm -r check-types" }, "keywords": [ From 0ff9903f5b452db417a18a95fa856d46b547fa29 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Tue, 14 Apr 2026 18:05:42 +0200 Subject: [PATCH 05/29] fix disparities --- .../scripts/codemod.ts | 7 +- .../expected.tsx | 8 +++ .../input.tsx | 8 +++ .../scripts/codemod.ts | 2 +- .../nested-member-no-change/expected.tsx | 7 ++ .../tests/nested-member-no-change/input.tsx | 7 ++ .../remove-forward-ref/scripts/codemod.ts | 65 ++++++++++++++++--- .../expected.tsx | 5 ++ .../invalid-props-pattern-no-change/input.tsx | 5 ++ .../invalid-render-arg-no-change/expected.tsx | 3 + .../invalid-render-arg-no-change/input.tsx | 3 + .../tests/multiple-react-imports/expected.tsx | 10 +++ .../tests/multiple-react-imports/input.tsx | 6 ++ .../tests/multiple-react-imports/metrics.json | 10 +++ .../typescript-qualified-names/expected.tsx | 10 +++ .../typescript-qualified-names/input.tsx | 8 +++ .../typescript-qualified-names/metrics.json | 10 +++ .../expected.tsx | 11 ++++ .../input.tsx | 11 ++++ .../metrics.json | 10 +++ .../expected.tsx | 11 ++++ .../input.tsx | 8 +++ .../metrics.json | 10 +++ 23 files changed, 225 insertions(+), 10 deletions(-) create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/expected.tsx create mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/input.tsx create mode 100644 codemods/remove-context-provider/tests/nested-member-no-change/expected.tsx create mode 100644 codemods/remove-context-provider/tests/nested-member-no-change/input.tsx create mode 100644 codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/input.tsx create mode 100644 codemods/remove-forward-ref/tests/invalid-render-arg-no-change/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/invalid-render-arg-no-change/input.tsx create mode 100644 codemods/remove-forward-ref/tests/multiple-react-imports/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/multiple-react-imports/input.tsx create mode 100644 codemods/remove-forward-ref/tests/multiple-react-imports/metrics.json create mode 100644 codemods/remove-forward-ref/tests/typescript-qualified-names/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-qualified-names/input.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-qualified-names/metrics.json create mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-props-union/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-props-union/input.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-props-union/metrics.json create mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/expected.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/input.tsx create mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/metrics.json diff --git a/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts b/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts index d6d2408..06dd5ed 100644 --- a/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts +++ b/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts @@ -89,7 +89,12 @@ function isReactDomDestructurePattern(node: SgNode): boolean { } const init = declarator.field("value"); - return isReactIdentifier(init) || isReactRequireCall(init); + if (!(isReactIdentifier(init) || isReactRequireCall(init))) { + return false; + } + + return node.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) + .some((property) => property.text() === "DOM"); } const transform: Transform = async (root) => { diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/expected.tsx new file mode 100644 index 0000000..0cdce6e --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/expected.tsx @@ -0,0 +1,8 @@ +const React = require('react'); +const { Component } = React; + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/input.tsx new file mode 100644 index 0000000..0cdce6e --- /dev/null +++ b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/input.tsx @@ -0,0 +1,8 @@ +const React = require('react'); +const { Component } = React; + +class Hello extends Component { + render() { + return DOM.div(null, `Hello ${this.props.toWhat}`); + } +} diff --git a/codemods/remove-context-provider/scripts/codemod.ts b/codemods/remove-context-provider/scripts/codemod.ts index 10480be..4670c2a 100644 --- a/codemods/remove-context-provider/scripts/codemod.ts +++ b/codemods/remove-context-provider/scripts/codemod.ts @@ -14,7 +14,7 @@ function isContextProvider(node: SgNode): boolean { const object = node.field("object"); const property = node.field("property"); - if (!object || !property || property.text() !== "Provider") { + if (!object || object.kind() !== "identifier" || !property || property.text() !== "Provider") { return false; } diff --git a/codemods/remove-context-provider/tests/nested-member-no-change/expected.tsx b/codemods/remove-context-provider/tests/nested-member-no-change/expected.tsx new file mode 100644 index 0000000..28d0643 --- /dev/null +++ b/codemods/remove-context-provider/tests/nested-member-no-change/expected.tsx @@ -0,0 +1,7 @@ +function App() { + return ( + + + + ); +} diff --git a/codemods/remove-context-provider/tests/nested-member-no-change/input.tsx b/codemods/remove-context-provider/tests/nested-member-no-change/input.tsx new file mode 100644 index 0000000..28d0643 --- /dev/null +++ b/codemods/remove-context-provider/tests/nested-member-no-change/input.tsx @@ -0,0 +1,7 @@ +function App() { + return ( + + + + ); +} diff --git a/codemods/remove-forward-ref/scripts/codemod.ts b/codemods/remove-forward-ref/scripts/codemod.ts index a3ab1fc..17fc65d 100644 --- a/codemods/remove-forward-ref/scripts/codemod.ts +++ b/codemods/remove-forward-ref/scripts/codemod.ts @@ -37,6 +37,52 @@ function parameterTypeAnnotation(param: SgNode): SgNode | null { return children.length > 1 ? children[1] ?? null : null; } +function legacyLiteralOrReferenceType(node: SgNode | null): SgNode | null { + const directNode = node && ( + node.kind() === "type_identifier" || + node.kind() === "nested_type_identifier" || + node.kind() === "object_type" || + node.kind() === "generic_type" + ) ? node : null; + const typeNode = directNode ?? ( + node && (node.kind() === "type_annotation" || node.kind() === "type_arguments") + ? firstNamedChild(node) + : null + ); + if (!typeNode) { + return null; + } + + return ( + typeNode.kind() === "type_identifier" || + typeNode.kind() === "nested_type_identifier" || + typeNode.kind() === "object_type" || + typeNode.kind() === "generic_type" + ) ? typeNode : null; +} + +function legacyReferenceType(node: SgNode | null): SgNode | null { + const directNode = node && ( + node.kind() === "type_identifier" || + node.kind() === "nested_type_identifier" || + node.kind() === "generic_type" + ) ? node : null; + const typeNode = directNode ?? ( + node && (node.kind() === "type_annotation" || node.kind() === "type_arguments") + ? firstNamedChild(node) + : null + ); + if (!typeNode) { + return null; + } + + return ( + typeNode.kind() === "type_identifier" || + typeNode.kind() === "nested_type_identifier" || + typeNode.kind() === "generic_type" + ) ? typeNode : null; +} + function stripLeadingColon(text: string): string { return text.startsWith(":") ? text.slice(1).trim() : text.trim(); } @@ -47,7 +93,7 @@ function callTypeArguments(call: SgNode): SgNode[] { } function refTypeFromForwardedRef(typeAnnotation: SgNode | null): string | null { - const typeNode = firstNamedChild(typeAnnotation); + const typeNode = legacyReferenceType(typeAnnotation); if (!typeNode || typeNode.kind() !== "generic_type") { return null; } @@ -65,7 +111,8 @@ function refTypeFromForwardedRef(typeAnnotation: SgNode | null): string | n return null; } - return firstNamedChild(genericArgs)?.text() ?? null; + const refType = legacyReferenceType(genericArgs); + return refType?.text() ?? null; } function refBindingEntry(refName: string): string { @@ -99,19 +146,21 @@ function propsPatternText(propsParam: SgNode, refName: string): string | nu function paramTypeText(propsParam: SgNode, call: SgNode, refParam: SgNode): string | null { const typeArgs = callTypeArguments(call); - const callRefType = typeArgs[0]?.text() ?? null; - const callPropsType = typeArgs[1]?.text() ?? null; - if (callPropsType) { - return `${callPropsType} & { ref: React.RefObject<${callRefType ?? "unknown"}> }`; + const refTypeArg = typeArgs[0] ?? null; + const propTypeArg = typeArgs[1] ?? null; + const legacyRefTypeArg = legacyReferenceType(refTypeArg); + const legacyPropTypeArg = legacyLiteralOrReferenceType(propTypeArg); + if (legacyRefTypeArg && legacyPropTypeArg) { + return `${legacyPropTypeArg.text()} & { ref: React.RefObject<${legacyRefTypeArg.text()}> }`; } - const propsType = parameterTypeAnnotation(propsParam); + const propsType = legacyLiteralOrReferenceType(parameterTypeAnnotation(propsParam)); if (!propsType) { return null; } const refType = refTypeFromForwardedRef(parameterTypeAnnotation(refParam)) ?? "unknown"; - return `${stripLeadingColon(propsType.text())} & { ref: React.RefObject<${refType}> }`; + return `${propsType.text()} & { ref: React.RefObject<${refType}> }`; } function bodyText(fn: SgNode): string | null { diff --git a/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/expected.tsx b/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/expected.tsx new file mode 100644 index 0000000..dc5bb49 --- /dev/null +++ b/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/expected.tsx @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyComponent = forwardRef(function Component([props], ref) { + return null; +}); diff --git a/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/input.tsx b/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/input.tsx new file mode 100644 index 0000000..dc5bb49 --- /dev/null +++ b/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/input.tsx @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyComponent = forwardRef(function Component([props], ref) { + return null; +}); diff --git a/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/expected.tsx b/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/expected.tsx new file mode 100644 index 0000000..a4303f4 --- /dev/null +++ b/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/expected.tsx @@ -0,0 +1,3 @@ +import { forwardRef } from 'react'; + +const MyComponent = forwardRef(null); diff --git a/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/input.tsx b/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/input.tsx new file mode 100644 index 0000000..a4303f4 --- /dev/null +++ b/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/input.tsx @@ -0,0 +1,3 @@ +import { forwardRef } from 'react'; + +const MyComponent = forwardRef(null); diff --git a/codemods/remove-forward-ref/tests/multiple-react-imports/expected.tsx b/codemods/remove-forward-ref/tests/multiple-react-imports/expected.tsx new file mode 100644 index 0000000..479c98b --- /dev/null +++ b/codemods/remove-forward-ref/tests/multiple-react-imports/expected.tsx @@ -0,0 +1,10 @@ +import { useState } from 'react'; + +const MyComponent = ( + { + ref, + ...props + } +) => { + return null; +}; diff --git a/codemods/remove-forward-ref/tests/multiple-react-imports/input.tsx b/codemods/remove-forward-ref/tests/multiple-react-imports/input.tsx new file mode 100644 index 0000000..5b6e9be --- /dev/null +++ b/codemods/remove-forward-ref/tests/multiple-react-imports/input.tsx @@ -0,0 +1,6 @@ +import { useState } from 'react'; +import { forwardRef } from 'react'; + +const MyComponent = forwardRef((props, ref) => { + return null; +}); diff --git a/codemods/remove-forward-ref/tests/multiple-react-imports/metrics.json b/codemods/remove-forward-ref/tests/multiple-react-imports/metrics.json new file mode 100644 index 0000000..eb74e11 --- /dev/null +++ b/codemods/remove-forward-ref/tests/multiple-react-imports/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/multiple-react-imports/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-qualified-names/expected.tsx b/codemods/remove-forward-ref/tests/typescript-qualified-names/expected.tsx new file mode 100644 index 0000000..5c8103e --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-qualified-names/expected.tsx @@ -0,0 +1,10 @@ +const MyComponent = function Component( + { + ref: myRef, + ...myProps + }: NS.Props & { + ref: React.RefObject + } +) { + return null; +}; diff --git a/codemods/remove-forward-ref/tests/typescript-qualified-names/input.tsx b/codemods/remove-forward-ref/tests/typescript-qualified-names/input.tsx new file mode 100644 index 0000000..833d99c --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-qualified-names/input.tsx @@ -0,0 +1,8 @@ +import { forwardRef } from 'react'; + +const MyComponent = forwardRef(function Component( + myProps: NS.Props, + myRef: React.ForwardedRef +) { + return null; +}); diff --git a/codemods/remove-forward-ref/tests/typescript-qualified-names/metrics.json b/codemods/remove-forward-ref/tests/typescript-qualified-names/metrics.json new file mode 100644 index 0000000..228e615 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-qualified-names/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/typescript-qualified-names/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/expected.tsx b/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/expected.tsx new file mode 100644 index 0000000..24c40ee --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/expected.tsx @@ -0,0 +1,11 @@ +type A = { a: 1 }; +type B = { b: 1 }; + +const MyComponent = function Component( + { + ref: myRef, + ...myProps + } +) { + return null; +}; diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/input.tsx b/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/input.tsx new file mode 100644 index 0000000..2182401 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/input.tsx @@ -0,0 +1,11 @@ +import { forwardRef } from 'react'; + +type A = { a: 1 }; +type B = { b: 1 }; + +const MyComponent = forwardRef(function Component( + myProps: A | B, + myRef: React.ForwardedRef +) { + return null; +}); diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/metrics.json b/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/metrics.json new file mode 100644 index 0000000..5d14c6e --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/typescript-unsupported-props-union/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/expected.tsx b/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/expected.tsx new file mode 100644 index 0000000..6710ae3 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/expected.tsx @@ -0,0 +1,11 @@ +type A = { a: 1 }; +type B = { b: 1 }; + +const MyComponent = ( + { + ref, + ...props + } +) => { + return null; +}; diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/input.tsx b/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/input.tsx new file mode 100644 index 0000000..f7d4852 --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/input.tsx @@ -0,0 +1,8 @@ +import { forwardRef } from 'react'; + +type A = { a: 1 }; +type B = { b: 1 }; + +const MyComponent = forwardRef((props, ref) => { + return null; +}); diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/metrics.json b/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/metrics.json new file mode 100644 index 0000000..772591e --- /dev/null +++ b/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/metrics.json @@ -0,0 +1,10 @@ +{ + "remove-forward-ref-replacements": [ + { + "cardinality": { + "file": "tests/typescript-unsupported-type-arguments-union/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file From e765020a2ca336313f717fb87c355170bc1d6071 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Tue, 14 Apr 2026 20:17:35 +0200 Subject: [PATCH 06/29] fix create-element-to-jsx --- codemods/create-element-to-jsx/package.json | 2 +- .../create-element-to-jsx/scripts/codemod.ts | 828 +++++++++++++++++- .../scripts/differential-tests.mjs | 103 +++ .../scripts/error-tests.mjs | 47 + .../expected.tsx | 4 + .../comment-only-props-string-child/input.tsx | 2 + .../metrics.json | 10 + .../member-child-comments-alt/expected.tsx | 4 + .../tests/member-child-comments-alt/input.tsx | 2 + .../member-child-comments-alt/metrics.json | 10 + .../tests/member-simple-comments/expected.tsx | 2 + .../tests/member-simple-comments/input.tsx | 2 + .../tests/member-simple-comments/metrics.json | 10 + .../expected.tsx | 3 + .../no-react-valid-explicit-require/input.tsx | 3 + .../metrics.json | 10 + .../test.config.json | 5 + .../tests/no-react-valid/expected.tsx | 3 + .../tests/no-react-valid/input.tsx | 3 + .../tests/preserve-comments/expected.tsx | 80 +- .../tests/spread-prop-comments/expected.tsx | 4 + .../tests/spread-prop-comments/input.tsx | 2 + .../tests/spread-prop-comments/metrics.json | 10 + 23 files changed, 1073 insertions(+), 76 deletions(-) create mode 100644 codemods/create-element-to-jsx/scripts/differential-tests.mjs create mode 100644 codemods/create-element-to-jsx/scripts/error-tests.mjs create mode 100644 codemods/create-element-to-jsx/tests/comment-only-props-string-child/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/comment-only-props-string-child/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/comment-only-props-string-child/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/member-child-comments-alt/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/member-child-comments-alt/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/member-child-comments-alt/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/member-simple-comments/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/member-simple-comments/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/member-simple-comments/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/metrics.json create mode 100644 codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/test.config.json create mode 100644 codemods/create-element-to-jsx/tests/no-react-valid/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/no-react-valid/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/spread-prop-comments/expected.tsx create mode 100644 codemods/create-element-to-jsx/tests/spread-prop-comments/input.tsx create mode 100644 codemods/create-element-to-jsx/tests/spread-prop-comments/metrics.json diff --git a/codemods/create-element-to-jsx/package.json b/codemods/create-element-to-jsx/package.json index 86bfbf6..743d67d 100644 --- a/codemods/create-element-to-jsx/package.json +++ b/codemods/create-element-to-jsx/package.json @@ -4,7 +4,7 @@ "description": "Convert React.createElement calls to JSX", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/error-tests.mjs ./scripts/differential-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/create-element-to-jsx/scripts/codemod.ts b/codemods/create-element-to-jsx/scripts/codemod.ts index 777f882..791c443 100644 --- a/codemods/create-element-to-jsx/scripts/codemod.ts +++ b/codemods/create-element-to-jsx/scripts/codemod.ts @@ -8,6 +8,31 @@ type Conversion = { hasComments: boolean; }; +type TagDescriptor = { + text: string; + trailingComments: string[]; +}; + +type AttrDescriptor = { + leadingBlocks: string[]; + inline: string; + hasComments: boolean; +}; + +type RenderedElement = { + core: string; + count: number; + leadingComments: string[]; + trailingComments: string[]; + hasComments: boolean; + wrappedChildTrailingComments?: string[]; +}; + +type ArgumentLayout = { + args: SgNode[]; + boundaryComments: SgNode[][]; +}; + const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); function metricFile(filename: string): string { @@ -164,6 +189,442 @@ function ownCommentTexts(call: SgNode): string[] { .map((comment) => comment.text()); } +function ownCommentNodes(call: SgNode): SgNode[] { + return call.findAll({ rule: { kind: "comment" } }) + .filter((comment) => closestCreateElementAncestor(comment)?.id() === call.id()) + .sort((left, right) => left.range().start.index - right.range().start.index); +} + +function directCommentNodes(node: SgNode | null): SgNode[] { + if (!node) { + return []; + } + + return node.children() + .filter((child) => child.kind() === "comment") + .sort((left, right) => left.range().start.index - right.range().start.index); +} + +function directCommentTexts(node: SgNode | null): string[] { + return directCommentNodes(node).map((comment) => comment.text()); +} + +function subtractComments(comments: SgNode[], excluded: SgNode[]): SgNode[] { + const excludedIds = new Set(excluded.map((comment) => comment.id())); + return comments.filter((comment) => !excludedIds.has(comment.id())); +} + +function splitDirectComments(node: SgNode, comments = directCommentNodes(node)): { + before: SgNode[]; + after: SgNode[]; + all: SgNode[]; +} { + const nodeText = node.text(); + const nodeStart = node.range().start.index; + const nodeEnd = node.range().end.index; + let tokenStart = nodeStart; + let tokenEnd = nodeEnd; + + if (comments.length > 0) { + const start = Math.min(nodeStart, ...comments.map((comment) => comment.range().start.index)); + const end = Math.max(nodeEnd, ...comments.map((comment) => comment.range().end.index)); + const segment = node.getRoot().root().text().slice(start, end); + const tokenOffset = segment.indexOf(nodeText); + if (tokenOffset >= 0) { + tokenStart = start + tokenOffset; + tokenEnd = tokenStart + nodeText.length; + } + } + + return { + before: comments.filter((comment) => comment.range().end.index <= tokenStart), + after: comments.filter((comment) => comment.range().start.index >= tokenEnd), + all: comments, + }; +} + +function joinCommentTexts(comments: SgNode[], source: string): string { + if (comments.length === 0) { + return ""; + } + + let text = comments[0]!.text(); + for (let i = 1; i < comments.length; i++) { + const previous = comments[i - 1]!; + const current = comments[i]!; + const between = source.slice(previous.range().end.index, current.range().start.index); + text += between.includes("\n") ? "" : between; + text += current.text(); + } + + return text; +} + +function commentsPrefixText(comments: SgNode[], nextStart: number, source: string): string { + if (comments.length === 0) { + return ""; + } + + const last = comments[comments.length - 1]!; + return `${joinCommentTexts(comments, source)}${source.slice(last.range().end.index, nextStart)}`; +} + +function trailingInlineCommentTexts(comments: SgNode[], anchorEnd: number, source: string): string[] { + if (comments.length === 0) { + return []; + } + + const texts: string[] = []; + let cursor = anchorEnd; + for (const comment of comments) { + texts.push(`${source.slice(cursor, comment.range().start.index)}${comment.text()}`); + cursor = comment.range().end.index; + } + return texts; +} + +function tokenSourceWithComments(node: SgNode, rendered: string, source: string): string { + const split = splitDirectComments(node); + const beforeStart = split.before.length > 0 + ? Math.min(...split.before.map((comment) => comment.range().start.index)) + : node.range().start.index; + const beforeText = source.slice(beforeStart, node.range().start.index); + const afterEnd = split.after.length > 0 + ? Math.max(...split.after.map((comment) => comment.range().end.index)) + : node.range().end.index; + const afterText = source.slice(node.range().end.index, afterEnd); + return `${beforeText}${rendered}${afterText}`; +} + +function commentBlocks(comments: SgNode[], source: string): string[] { + if (comments.length === 0) { + return []; + } + + const blocks = [comments[0]!.text()]; + for (let i = 1; i < comments.length; i++) { + const previous = comments[i - 1]!; + const current = comments[i]!; + const between = source.slice(previous.range().end.index, current.range().start.index); + if (between.includes("\n")) { + blocks.push(current.text()); + } else { + blocks[blocks.length - 1] += between + current.text(); + } + } + + return blocks; +} + +function isLineComment(text: string): boolean { + return text.startsWith("//"); +} + +function argumentLayout(call: SgNode): ArgumentLayout { + const argsNode = call.field("arguments"); + if (!argsNode) { + return { args: [], boundaryComments: [[]] }; + } + + const rawChildren = argsNode.children(); + const args: SgNode[] = []; + const boundaryComments: SgNode[][] = [[]]; + let seenArgs = 0; + + for (const child of rawChildren) { + if (child.kind() === "comment") { + boundaryComments[seenArgs]!.push(child); + continue; + } + + if (child.isNamed()) { + args.push(child); + seenArgs++; + if (!boundaryComments[seenArgs]) { + boundaryComments[seenArgs] = []; + } + } + } + + return { args, boundaryComments }; +} + +function detachedLeadingComments(call: SgNode): SgNode[] { + const parent = call.parent(); + if (!parent || parent.kind() === "arguments" || parent.kind() === "call_expression") { + return []; + } + + return parent.children() + .filter((child) => child.kind() === "comment" && child.range().end.index <= call.range().start.index) + .sort((left, right) => left.range().start.index - right.range().start.index); +} + +function splitBoundaryCommentsByComma( + anchorNode: SgNode, + comments: SgNode[], + source: string, +): { beforeComma: SgNode[]; afterComma: SgNode[] } { + const beforeComma: SgNode[] = []; + const afterComma: SgNode[] = []; + for (const comment of comments) { + const between = source.slice(anchorNode.range().end.index, comment.range().start.index); + if (between.includes(",")) { + afterComma.push(comment); + } else { + beforeComma.push(comment); + } + } + return { beforeComma, afterComma }; +} + +function tagDescriptor(node: SgNode): TagDescriptor | null { + if (node.kind() === "identifier" || node.kind() === "property_identifier") { + return { text: node.text(), trailingComments: directCommentTexts(node) }; + } + + if (node.kind() === "string") { + return { text: stringValue(node), trailingComments: directCommentTexts(node) }; + } + + if (node.kind() !== "member_expression") { + return null; + } + + const object = node.field("object"); + const property = node.field("property"); + if (!object || !property) { + return null; + } + + const objectTag = tagDescriptor(object); + const propertyTag = tagDescriptor(property); + if (!objectTag || !propertyTag) { + return null; + } + + const subtreeComments = node.findAll({ rule: { kind: "comment" } }) + .sort((left, right) => left.range().start.index - right.range().start.index) + .map((comment) => comment.text()); + const trailingComments = [ + ...directCommentTexts(node), + ...objectTag.trailingComments, + ...propertyTag.trailingComments, + ]; + + return { + text: `${objectTag.text}.${propertyTag.text}`, + trailingComments, + }; +} + +function renderTokenWithComments(node: SgNode, text: string, source: string): string { + return tokenSourceWithComments(node, text, source); +} + +function renderPropValue(node: SgNode, source: string): string { + if (node.kind() === "string") { + const literal = stringValue(node); + if (canLiteralBePropString(node, literal)) { + return renderTokenWithComments(node, `"${literal}"`, source); + } + } + + const split = splitDirectComments(node); + return `{${joinCommentTexts(split.before, source)}${node.text()}${joinCommentTexts(split.after, source)}}`; +} + +function spreadAttrDescriptor(node: SgNode, source: string): AttrDescriptor { + const argument = spreadArgument(node); + if (!argument) { + throw new Error("Expected spread element argument"); + } + + return { + leadingBlocks: commentBlocks(directCommentNodes(node), source), + inline: `{...${tokenSourceWithComments(argument, argument.text(), source)}}`, + hasComments: directCommentNodes(node).length > 0 || directCommentNodes(argument).length > 0, + }; +} + +function pairInlineDescriptor( + node: SgNode, + source: string, + prefixInlineComments: string[], + suffixInlineComments: string[], + leadingBlocks: string[], +): AttrDescriptor { + const key = node.field("key"); + const value = node.field("value"); + if (!key || !value) { + throw new Error("Expected object property to have key and value"); + } + + const pairComments = directCommentNodes(node); + const keyAfter = pairComments[0] ? [pairComments[0]!] : []; + const valueBefore = pairComments.slice(1); + const keyText = `${prefixInlineComments.join("")}${attrName(key)}${joinCommentTexts(keyAfter, source)}`; + const valueText = (() => { + if (value.kind() === "string") { + const literal = stringValue(value); + if (canLiteralBePropString(value, literal)) { + return `${commentsPrefixText(valueBefore, value.range().start.index, source)}"${literal}"${suffixInlineComments.join("")}`; + } + } + return `{${commentsPrefixText(valueBefore, value.range().start.index, source)}${value.text()}}${suffixInlineComments.join("")}`; + })(); + + return { + leadingBlocks, + inline: `${keyText}=${valueText}`, + hasComments: + leadingBlocks.length > 0 || + prefixInlineComments.length > 0 || + suffixInlineComments.length > 0 || + pairComments.length > 0, + }; +} + +function propsToDescriptors(node: SgNode | null, source: string): { + attrs: AttrDescriptor[]; + trailingElementComments: string[]; + consumedBoundaryComments: string[]; +} { + if (!node || node.kind() === "null") { + return { attrs: [], trailingElementComments: [], consumedBoundaryComments: [] }; + } + + if (isMemberCall(node, "React", "__spread") || isMemberCall(node, "Object", "assign")) { + const callee = node.field("function"); + const calleeObject = callee?.field("object") ?? null; + const calleeProperty = callee?.field("property") ?? null; + const attrs: AttrDescriptor[] = []; + const trailing = [ + ...directCommentTexts(node), + ...directCommentTexts(callee), + ...directCommentTexts(calleeObject), + ...directCommentTexts(calleeProperty), + ]; + + for (const arg of callArguments(node)) { + const nested = propsToDescriptors(arg, source); + attrs.push(...nested.attrs); + trailing.push(...nested.trailingElementComments); + } + + return { attrs, trailingElementComments: trailing, consumedBoundaryComments: [] }; + } + + if ( + node.kind() === "identifier" || + node.kind() === "member_expression" || + node.kind() === "call_expression" + ) { + return { + attrs: [{ + leadingBlocks: [], + inline: `{...${tokenSourceWithComments(node, node.text(), source)}}`, + hasComments: directCommentNodes(node).length > 0, + }], + trailingElementComments: [], + consumedBoundaryComments: [], + }; + } + + if (node.kind() !== "object") { + throw new Error(`Unexpected attribute of type "${legacyType(node)}"`); + } + + const attrs: AttrDescriptor[] = []; + const objectComments = directCommentNodes(node); + const namedChildrenList = node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); + const pendingLeadingBlocks = new Map(); + const consumedObjectComments = new Set(); + + if (namedChildrenList.length === 0) { + return { + attrs, + trailingElementComments: objectComments.map((comment) => comment.text()), + consumedBoundaryComments: [], + }; + } + + for (let index = 0; index < namedChildrenList.length; index++) { + const child = namedChildrenList[index]!; + const nextChild = namedChildrenList[index + 1]; + + if (child.kind() === "spread_element") { + attrs.push(spreadAttrDescriptor(child, source)); + continue; + } + + if (child.kind() !== "pair") { + continue; + } + + const key = child.field("key"); + const value = child.field("value"); + if (!key || !value) { + continue; + } + + const commentsBefore = objectComments.filter((comment) => + !consumedObjectComments.has(comment.id()) && + comment.range().end.index <= key.range().start.index && + comment.range().start.index >= node.range().start.index + ); + const commentsAfterValue = objectComments.filter((comment) => + !consumedObjectComments.has(comment.id()) && + comment.range().start.index >= value.range().end.index && + comment.range().end.index <= (nextChild?.range().start.index ?? node.range().end.index) + ); + + const beforeComma: SgNode[] = []; + const afterComma: SgNode[] = []; + for (const comment of commentsAfterValue) { + const between = source.slice(value.range().end.index, comment.range().start.index); + if (between.includes(",")) { + afterComma.push(comment); + } else { + beforeComma.push(comment); + } + } + + const trailingBlocksForCurrent = !nextChild + ? (commentsBefore.length > 0 && afterComma.length > 0 + ? [`${commentBlocks(commentsBefore, source).join("")}${afterComma.map((comment) => comment.text()).join("")}`] + : commentBlocks(afterComma, source)) + : []; + const currentLeadingBlocks = [ + ...(pendingLeadingBlocks.get(index) ?? []), + ...((!nextChild && afterComma.length > 0) + ? [] + : (index === 0 ? [] : commentBlocks(commentsBefore, source))), + ...trailingBlocksForCurrent, + ]; + const inlinePrefix = index === 0 ? commentsBefore.map((comment) => comment.text()) : []; + const inlineSuffix = beforeComma.map((comment) => comment.text()); + + attrs.push(pairInlineDescriptor(child, source, inlinePrefix, inlineSuffix, currentLeadingBlocks)); + [...commentsBefore, ...beforeComma, ...afterComma].forEach((comment) => consumedObjectComments.add(comment.id())); + + const carryBlocks = nextChild ? commentBlocks(afterComma, source) : []; + if (carryBlocks.length > 0) { + const targetIndex = nextChild ? index + 1 : index; + pendingLeadingBlocks.set(targetIndex, [ + ...(pendingLeadingBlocks.get(targetIndex) ?? []), + ...carryBlocks, + ]); + } + } + + return { + attrs, + trailingElementComments: [], + consumedBoundaryComments: [], + }; +} + function jsxReference(node: SgNode): string | null { if (node.kind() === "identifier" || node.kind() === "property_identifier") { return node.text(); @@ -346,7 +807,7 @@ function renderExpression(code: string, baseIndent: string): string { return `{\n${indentAll(code, `${baseIndent} `)}\n${baseIndent}}`; } -function renderChild(node: SgNode, baseIndent: string): { code: string; count: number } { +function renderSimpleChild(node: SgNode, baseIndent: string): { code: string; count: number } { if (node.kind() === "string") { const value = stringValue(node); if (value !== "" && value.trim() === value) { @@ -366,7 +827,7 @@ function renderChild(node: SgNode, baseIndent: string): { code: string; cou } if (isReactCreateElementCall(node)) { - const nested = convertCall(node, baseIndent); + const nested = convertSimpleCall(node, baseIndent); if (!nested) { return { code: renderExpression(node.text(), baseIndent), count: 0 }; } @@ -380,13 +841,7 @@ function renderChild(node: SgNode, baseIndent: string): { code: string; cou return { code: renderExpression(node.text(), baseIndent), count: 0 }; } -function wrapWithComments(jsx: string, comments: string[], baseIndent: string): string { - const innerIndent = `${baseIndent} `; - const commentBlock = comments.map((comment) => `${innerIndent}${comment}`).join("\n"); - return `(\n${commentBlock}\n${indentAll(jsx, innerIndent)}\n${baseIndent})`; -} - -function convertCall(call: SgNode, baseIndent: string): Conversion | null { +function convertSimpleCall(call: SgNode, baseIndent: string): Conversion | null { const args = callArguments(call); const elementArg = args[0]; if (!elementArg) { @@ -401,7 +856,7 @@ function convertCall(call: SgNode, baseIndent: string): Conversion | null { const propsArg = args[1] ?? null; const attributes = propsToAttributes(propsArg); const childIndent = `${baseIndent} `; - const childResults = args.slice(2).map((child) => renderChild(child, childIndent)); + const childResults = args.slice(2).map((child) => renderSimpleChild(child, childIndent)); const attributeSuffix = attributes.length > 0 ? ` ${attributes.join(" ")}` : ""; let jsx = `<${tag}${attributeSuffix}`; @@ -417,15 +872,339 @@ function convertCall(call: SgNode, baseIndent: string): Conversion | null { jsx += `>\n${childLines.join("\n")}\n${baseIndent}`; } - const comments = ownCommentTexts(call); - if (comments.length === 0) { - return { code: jsx, count, hasComments: false }; + return { code: jsx, count, hasComments: false }; +} + +function renderAttributeDescriptor(attr: AttrDescriptor, indent: string): string { + const mergedBlocks = [...attr.leadingBlocks]; + if (mergedBlocks.length >= 2 && mergedBlocks[mergedBlocks.length - 1]!.startsWith("//")) { + mergedBlocks[mergedBlocks.length - 2] = `${mergedBlocks[mergedBlocks.length - 2]!}${mergedBlocks[mergedBlocks.length - 1]!}`; + mergedBlocks.pop(); + } + const lines = mergedBlocks.map((block) => `${indent}${block}`); + lines.push(`${indent}${attr.inline}`); + return lines.join("\n"); +} + +function renderExpressionContainer( + core: string, + leadingComments: string[], + trailingComments: string[], + _indent: string, +): string { + const leadingText = leadingComments.join(""); + const trailingText = trailingComments.join(""); + + if (leadingText.length === 0 && trailingText.length === 0 && !core.includes("\n")) { + return `{${core}}`; + } + + if (leadingText.length > 0) { + if (core.includes("\n")) { + return `{\n${core}${trailingText}\n}`; + } + if (!leadingComments.some(isLineComment)) { + return `{${leadingText}${core}${trailingText}}`; + } + return `{${leadingText}\n${core}${trailingText}}`; + } + + if (trailingText.length > 0 && !core.includes("\n")) { + return `{${core}${trailingText}}`; + } + + return `{\n${core}\n}`; +} + +function renderElementCore( + tag: string, + attrs: AttrDescriptor[], + children: { code: string; count: number }[], + baseIndent: string, +): string { + const attrIndent = `${baseIndent} `; + const hasCommentedAttrs = attrs.some((attr) => attr.leadingBlocks.length > 0); + + if (children.length === 0) { + if (attrs.length === 0) { + return `<${tag} />`; + } + + if (hasCommentedAttrs) { + return `<${tag}\n${attrs.map((attr) => renderAttributeDescriptor(attr, attrIndent)).join("\n")}\n${baseIndent}/>`; + } + + return `<${tag} ${attrs.map((attr) => attr.inline).join(" ")} />`; + } + + let opening = `<${tag}>`; + if (attrs.length > 0) { + if (hasCommentedAttrs) { + const attrLines = attrs.map((attr) => renderAttributeDescriptor(attr, attrIndent)); + attrLines[attrLines.length - 1] = `${attrLines[attrLines.length - 1]!}>`; + opening = `<${tag}\n${attrLines.join("\n")}`; + } else { + opening = `<${tag} ${attrs.map((attr) => attr.inline).join(" ")}>`; + } + } + + const childIndent = `${baseIndent} `; + const renderedChildren = children.map((child) => indentAll(child.code, childIndent)); + return `${opening}\n${renderedChildren.join("\n")}\n${baseIndent}`; +} + +function finalizeRenderedElement(rendered: RenderedElement, baseIndent: string): string { + if (!rendered.hasComments) { + return rendered.core; + } + + if (!rendered.core.includes("\n") && rendered.leadingComments.length === 0 && rendered.trailingComments.length > 0) { + return `${rendered.core}${rendered.trailingComments.join("")}`; + } + + if (rendered.leadingComments.length === 0 && rendered.trailingComments.length > 0) { + return `${rendered.core}${rendered.trailingComments.join("")}`; + } + + const innerIndent = `${baseIndent} `; + const lines: string[] = []; + if (rendered.leadingComments.length > 0) { + if (rendered.leadingComments.some(isLineComment)) { + lines.push(...rendered.leadingComments.map((comment) => `${innerIndent}${comment}`)); + lines.push(indentAll(rendered.core, innerIndent)); + } else { + const indentedCore = indentAll(rendered.core, innerIndent); + const coreLines = indentedCore.split("\n"); + coreLines[0] = `${innerIndent}${rendered.leadingComments.join("")}${coreLines[0]!.slice(innerIndent.length)}`; + lines.push(coreLines.join("\n")); + } + } else { + lines.push(indentAll(rendered.core, innerIndent)); + } + + if (rendered.trailingComments.length > 0) { + lines.push(`${innerIndent}${rendered.trailingComments.join("")}`); + } + + return `(\n${lines.join("\n")}\n${baseIndent})`; +} + +function commentedChild( + node: SgNode, + leadingBoundaryComments: SgNode[], + trailingBoundaryComments: SgNode[], + baseIndent: string, + source: string, +): { code: string; count: number } { + let boundaryLeading = [ + ...leadingBoundaryComments.map((comment) => comment.text()), + ...trailingBoundaryComments.map((comment) => comment.text()), + ]; + let trailingInlineComments: string[] = []; + if (isReactCreateElementCall(node) && trailingBoundaryComments.length > 0) { + const split = splitBoundaryCommentsByComma(node, trailingBoundaryComments, source); + boundaryLeading = [ + ...leadingBoundaryComments.map((comment) => comment.text()), + ...split.beforeComma.filter((comment) => isLineComment(comment.text())).map((comment) => comment.text()), + ...split.afterComma.map((comment) => comment.text()), + ]; + trailingInlineComments = split.beforeComma + .filter((comment) => !isLineComment(comment.text())) + .map((comment) => comment.text()); + } + const directLeading = isReactCreateElementCall(node) ? [] : directCommentTexts(node); + const leadingComments = [...boundaryLeading, ...directLeading]; + + if (node.kind() === "string") { + const value = stringValue(node); + if (leadingComments.length === 0 && value !== "" && value.trim() === value) { + return { code: encodeJSXTextValue(value), count: 0 }; + } + + return { + code: renderExpressionContainer(node.text(), leadingComments, [], baseIndent), + count: 0, + }; + } + + if (node.kind() === "spread_element") { + const argument = spreadArgument(node); + if (!argument) { + throw new Error("Expected spread child argument"); + } + + return { + code: renderExpressionContainer(argument.text(), leadingComments, [], baseIndent), + count: 0, + }; + } + + if (isReactCreateElementCall(node)) { + const nested = convertCommentedCall(node, baseIndent, source); + if (!nested) { + return { + code: renderExpressionContainer(node.text(), leadingComments, [], baseIndent), + count: 0, + }; + } + + if (!nested.hasComments && leadingComments.length === 0) { + return { code: nested.core, count: nested.count }; + } + + return { + code: renderExpressionContainer( + nested.core, + [...leadingComments, ...nested.leadingComments], + [ + ...trailingInlineCommentTexts( + trailingBoundaryComments.filter((comment) => !isLineComment(comment.text())), + node.range().end.index, + source, + ), + ...(nested.wrappedChildTrailingComments ?? nested.trailingComments), + ], + baseIndent, + ), + count: nested.count, + }; + } + + return { + code: renderExpressionContainer(node.text(), leadingComments, [], baseIndent), + count: 0, + }; +} + +function convertCommentedCall(call: SgNode, baseIndent: string, source: string): RenderedElement | null { + const layout = argumentLayout(call); + const args = layout.args; + const elementArg = args[0]; + if (!elementArg) { + return null; + } + + const tag = topLevelJsxTag(elementArg); + if (!tag) { + return null; + } + + const tagInfo = tagDescriptor(elementArg); + if (!tagInfo) { + return null; + } + + const callLeadingComments = [ + ...detachedLeadingComments(call).map((comment) => comment.text()), + ...directCommentTexts(call), + ]; + const callee = call.field("function"); + const calleeLeadingComments = [ + ...directCommentTexts(callee), + ...directCommentTexts(callee?.field("object") ?? null), + ...directCommentTexts(callee?.field("property") ?? null), + ]; + + const beforeFirst = layout.boundaryComments[0] ?? []; + const betweenTagAndProps = layout.boundaryComments[1] ?? []; + const propsArg = args[1] ?? null; + const propsIsSpreadLike = propsArg !== null && + (propsArg.kind() === "identifier" || propsArg.kind() === "member_expression" || propsArg.kind() === "call_expression"); + const propsIsNull = propsArg?.kind() === "null"; + const leadingComments = [ + ...callLeadingComments, + ...calleeLeadingComments, + ...beforeFirst.filter((comment) => isLineComment(comment.text())).map((comment) => comment.text()), + ...betweenTagAndProps.filter((comment) => isLineComment(comment.text())).map((comment) => comment.text()), + ]; + const baseTagPropsTrailing = (propsIsSpreadLike || propsIsNull) + ? [] + : betweenTagAndProps.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()); + const memberNoPropsBoundaryComments = !propsArg && elementArg.kind() === "member_expression" + ? [ + ...beforeFirst.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()), + ...baseTagPropsTrailing, + ] + : []; + const trailingComments = !propsArg && elementArg.kind() === "member_expression" + ? [ + ...beforeFirst.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()), + ...baseTagPropsTrailing, + ...tagInfo.trailingComments, + ] + : [ + ...tagInfo.trailingComments, + ...beforeFirst.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()), + ...baseTagPropsTrailing, + ]; + + const propsBoundaryAfter = propsArg && args.length === 2 + ? (layout.boundaryComments[2] ?? []) + : []; + const props = propsToDescriptors(propsArg, source); + if (propsArg && (propsArg.kind() === "identifier" || propsArg.kind() === "member_expression" || propsArg.kind() === "call_expression")) { + const spreadAttr = props.attrs[0]; + if (spreadAttr) { + const beforeText = betweenTagAndProps.map((comment) => comment.text()).join(""); + const afterText = propsBoundaryAfter.map((comment) => comment.text()).join(""); + spreadAttr.inline = `{...${beforeText}${propsArg.text()}${afterText}}`; + spreadAttr.hasComments ||= beforeText.length > 0 || afterText.length > 0; + } + } + if (propsIsNull) { + const nullPropComments = [ + ...betweenTagAndProps.map((comment) => comment.text()), + ...propsBoundaryAfter.map((comment) => comment.text()), + ]; + if (nullPropComments.length > 0) { + trailingComments.push(` ${nullPropComments[0]!}`, ...nullPropComments.slice(1)); + } + } + trailingComments.push(...props.trailingElementComments); + + const childIndent = `${baseIndent} `; + const children = args.slice(2).map((child, index) => { + const argIndex = index + 2; + let boundaryBefore = argIndex === 2 ? (layout.boundaryComments[argIndex] ?? []) : []; + if (argIndex === 2 && propsArg) { + const split = splitBoundaryCommentsByComma(propsArg, boundaryBefore, source); + if (propsIsSpreadLike) { + boundaryBefore = [...split.beforeComma, ...split.afterComma]; + } else { + trailingComments.push(...split.beforeComma.map((comment) => comment.text())); + boundaryBefore = split.afterComma; + } + } + const boundaryAfter = layout.boundaryComments[argIndex + 1] ?? []; + return commentedChild(child, boundaryBefore, boundaryAfter, childIndent, source); + }); + + const core = renderElementCore(tagInfo.text, props.attrs, children, ""); + let count = 1; + for (const child of children) { + count += child.count; } return { - code: wrapWithComments(jsx, comments, baseIndent), + core, count, - hasComments: true, + leadingComments, + trailingComments, + hasComments: + leadingComments.length > 0 || + trailingComments.length > 0 || + props.attrs.some((attr) => attr.leadingBlocks.length > 0) || + children.some((child) => child.code.includes("\n")), + wrappedChildTrailingComments: + !propsArg && elementArg.kind() === "member_expression" && memberNoPropsBoundaryComments.length > 0 + ? [ + ...memberNoPropsBoundaryComments, + ...tagInfo.trailingComments, + ...memberNoPropsBoundaryComments, + ...tagInfo.trailingComments, + ...tagInfo.trailingComments, + ] + : undefined, }; } @@ -470,12 +1249,27 @@ const transform: Transform = async (root, options) => { } const baseIndent = lineIndent(source, call.range().start.index); - const conversion = convertCall(call, baseIndent); + const conversion = ownCommentNodes(call).length > 0 + ? convertCommentedCall(call, baseIndent, source) + : convertSimpleCall(call, baseIndent); if (!conversion) { continue; } - edits.push(call.replace(conversion.code)); + const code = "core" in conversion ? finalizeRenderedElement(conversion, baseIndent) : conversion.code; + if ("core" in conversion) { + const detachedComments = detachedLeadingComments(call); + const replacementStart = detachedComments.length > 0 + ? Math.min(...detachedComments.map((comment) => comment.range().start.index)) + : call.range().start.index; + edits.push({ + startPos: replacementStart, + endPos: call.range().end.index, + insertedText: code, + }); + } else { + edits.push(call.replace(code)); + } totalConversions += conversion.count; } diff --git a/codemods/create-element-to-jsx/scripts/differential-tests.mjs b/codemods/create-element-to-jsx/scripts/differential-tests.mjs new file mode 100644 index 0000000..1a855ab --- /dev/null +++ b/codemods/create-element-to-jsx/scripts/differential-tests.mjs @@ -0,0 +1,103 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); +const packageDir = path.dirname(path.dirname(__filename)); +const workflowPath = path.join(packageDir, "workflow.yaml"); +const legacyTransformPath = "/Users/mohabsameh/Downloads/react-codemod/transforms/create-element-to-jsx.js"; + +function runLegacy(source) { + const dir = mkdtempSync(path.join(tmpdir(), "ce2jsx-legacy-")); + const inputPath = path.join(dir, "input.js"); + writeFileSync(inputPath, source, "utf8"); + + try { + const result = spawnSync( + "npx", + [ + "--yes", + "jscodeshift", + "-t", + legacyTransformPath, + inputPath, + "--run-in-band", + "--parser", + "flow", + ], + { + cwd: packageDir, + encoding: "utf8", + timeout: 20000, + }, + ); + assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); + return readFileSync(inputPath, "utf8").trim(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +function runJssg(source) { + const dir = mkdtempSync(path.join(tmpdir(), "ce2jsx-jssg-")); + const inputPath = path.join(dir, "input.tsx"); + writeFileSync(inputPath, source, "utf8"); + writeFileSync(path.join(dir, ".gitignore"), "", "utf8"); + + try { + spawnSync("git", ["init", "-q"], { cwd: dir }); + spawnSync("git", ["add", "input.tsx", ".gitignore"], { cwd: dir }); + spawnSync("git", ["commit", "-qm", "init"], { cwd: dir }); + + const result = spawnSync( + "pnpm", + [ + "dlx", + "codemod@latest", + "workflow", + "run", + "-w", + workflowPath, + "--target", + dir, + "--allow-dirty", + ], + { + cwd: packageDir, + encoding: "utf8", + timeout: 20000, + }, + ); + assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); + return readFileSync(inputPath, "utf8").trim(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +const parityCases = { + "member_simple_comments": `var React = require('react'); +React.createElement(/*A*/Foo/*B*/./*C*/Bar/*D*/); +`, + "spread_prop_comments": `var React = require('react'); +React.createElement('div', /*A*/getProps()/*B*/, 'x'); +`, + "member_child_comments_alt": `var React = require('react'); +React.createElement('div', null, React.createElement(/*A*/Foo/*B*/./*C*/Bar/*D*/) /*E*/); +`, + "comment_only_props_string_child": `var React = require('react'); +React.createElement(Foo, {/*P*/}, 'x'); +`, +}; + +for (const [name, source] of Object.entries(parityCases)) { + test(`matches legacy output for ${name}`, () => { + const legacyOutput = runLegacy(source); + const jssgOutput = runJssg(source); + assert.strictEqual(jssgOutput, legacyOutput); + }); +} diff --git a/codemods/create-element-to-jsx/scripts/error-tests.mjs b/codemods/create-element-to-jsx/scripts/error-tests.mjs new file mode 100644 index 0000000..5480d1f --- /dev/null +++ b/codemods/create-element-to-jsx/scripts/error-tests.mjs @@ -0,0 +1,47 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); +const packageDir = path.dirname(path.dirname(__filename)); +const workflowPath = path.join(packageDir, "workflow.yaml"); + +function runCodemod(input) { + const target = mkdtempSync(path.join(tmpdir(), "create-element-to-jsx-")); + const filePath = path.join(target, "input.tsx"); + writeFileSync(filePath, input, "utf8"); + + try { + return spawnSync( + "pnpm", + [ + "dlx", + "codemod@latest", + "workflow", + "run", + "-w", + workflowPath, + "--target", + target, + "--allow-dirty", + ], + { + cwd: packageDir, + encoding: "utf8", + }, + ); + } finally { + rmSync(target, { recursive: true, force: true }); + } +} + +test("throws on unsupported props literal", () => { + const result = runCodemod("var React = require('react/addons');\nReact.createElement('foo', 1);\n"); + const combined = `${result.stdout}\n${result.stderr}`; + assert.match(combined, /Unexpected attribute of type .*Literal/); + assert.match(combined, /Failed to execute codemod|InitializationFailed/); +}); diff --git a/codemods/create-element-to-jsx/tests/comment-only-props-string-child/expected.tsx b/codemods/create-element-to-jsx/tests/comment-only-props-string-child/expected.tsx new file mode 100644 index 0000000..7b85a75 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/comment-only-props-string-child/expected.tsx @@ -0,0 +1,4 @@ +var React = require('react'); + + x +/*P*/; diff --git a/codemods/create-element-to-jsx/tests/comment-only-props-string-child/input.tsx b/codemods/create-element-to-jsx/tests/comment-only-props-string-child/input.tsx new file mode 100644 index 0000000..9f279a8 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/comment-only-props-string-child/input.tsx @@ -0,0 +1,2 @@ +var React = require('react'); +React.createElement(Foo, {/*P*/}, 'x'); diff --git a/codemods/create-element-to-jsx/tests/comment-only-props-string-child/metrics.json b/codemods/create-element-to-jsx/tests/comment-only-props-string-child/metrics.json new file mode 100644 index 0000000..df02d4e --- /dev/null +++ b/codemods/create-element-to-jsx/tests/comment-only-props-string-child/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/comment-only-props-string-child/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/member-child-comments-alt/expected.tsx b/codemods/create-element-to-jsx/tests/member-child-comments-alt/expected.tsx new file mode 100644 index 0000000..4d793b3 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/member-child-comments-alt/expected.tsx @@ -0,0 +1,4 @@ +var React = require('react'); +
    + { /*E*//*A*//*D*//*B*//*C*//*A*//*D*//*B*//*C*//*B*//*C*/} +
    ; diff --git a/codemods/create-element-to-jsx/tests/member-child-comments-alt/input.tsx b/codemods/create-element-to-jsx/tests/member-child-comments-alt/input.tsx new file mode 100644 index 0000000..067f04a --- /dev/null +++ b/codemods/create-element-to-jsx/tests/member-child-comments-alt/input.tsx @@ -0,0 +1,2 @@ +var React = require('react'); +React.createElement('div', null, React.createElement(/*A*/Foo/*B*/./*C*/Bar/*D*/) /*E*/); diff --git a/codemods/create-element-to-jsx/tests/member-child-comments-alt/metrics.json b/codemods/create-element-to-jsx/tests/member-child-comments-alt/metrics.json new file mode 100644 index 0000000..990a6a2 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/member-child-comments-alt/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/member-child-comments-alt/input.tsx" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/member-simple-comments/expected.tsx b/codemods/create-element-to-jsx/tests/member-simple-comments/expected.tsx new file mode 100644 index 0000000..db7c05f --- /dev/null +++ b/codemods/create-element-to-jsx/tests/member-simple-comments/expected.tsx @@ -0,0 +1,2 @@ +var React = require('react'); +/*A*//*D*//*B*//*C*/; diff --git a/codemods/create-element-to-jsx/tests/member-simple-comments/input.tsx b/codemods/create-element-to-jsx/tests/member-simple-comments/input.tsx new file mode 100644 index 0000000..f3c8928 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/member-simple-comments/input.tsx @@ -0,0 +1,2 @@ +var React = require('react'); +React.createElement(/*A*/Foo/*B*/./*C*/Bar/*D*/); diff --git a/codemods/create-element-to-jsx/tests/member-simple-comments/metrics.json b/codemods/create-element-to-jsx/tests/member-simple-comments/metrics.json new file mode 100644 index 0000000..f78cdca --- /dev/null +++ b/codemods/create-element-to-jsx/tests/member-simple-comments/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/member-simple-comments/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/expected.tsx b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/expected.tsx new file mode 100644 index 0000000..8fb7b0c --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/expected.tsx @@ -0,0 +1,3 @@ +var React = require('foo'); + +; diff --git a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/input.tsx b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/input.tsx new file mode 100644 index 0000000..789178e --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/input.tsx @@ -0,0 +1,3 @@ +var React = require('foo'); + +React.createElement(Foo, null); diff --git a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/metrics.json b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/metrics.json new file mode 100644 index 0000000..cccf0c4 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/no-react-valid-explicit-require/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/test.config.json b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/test.config.json new file mode 100644 index 0000000..4858435 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "explicit-require": false + } +} diff --git a/codemods/create-element-to-jsx/tests/no-react-valid/expected.tsx b/codemods/create-element-to-jsx/tests/no-react-valid/expected.tsx new file mode 100644 index 0000000..789178e --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-react-valid/expected.tsx @@ -0,0 +1,3 @@ +var React = require('foo'); + +React.createElement(Foo, null); diff --git a/codemods/create-element-to-jsx/tests/no-react-valid/input.tsx b/codemods/create-element-to-jsx/tests/no-react-valid/input.tsx new file mode 100644 index 0000000..789178e --- /dev/null +++ b/codemods/create-element-to-jsx/tests/no-react-valid/input.tsx @@ -0,0 +1,3 @@ +var React = require('foo'); + +React.createElement(Foo, null); diff --git a/codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx b/codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx index 945e2b0..28ca5ff 100644 --- a/codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx +++ b/codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx @@ -1,77 +1,41 @@ var React = require('react'); const render = () => { - return /*1*/( - /*2*/ - /*3*/ - /*4*/ - /*5*/ - /*6*/ - /*7*/ - /*8*/ - /*9*/ - /*10*/ - /*11*/ - /*12*/ - /*13*/ - /*14*/ - /*15*/ - /*16*/ - //17 - /*18*/ - /*19*/ - /*24*/ - //25 -
    - { - ( - /*20*/ - /*21*/ - /*22*/ - /*23*/ - - ) - } - { - ( - /*26*/ - /*27*/ - - ) - } - { - ( - /*28*/ - /*29*/ - - ) - } -
    + return ( + /*1*//*4*//*2*//*3*/
    + {/*19*///25 + /*24*//*20*//*23*//*21*//*22*//*20*//*23*//*21*//*22*//*21*//*22*/} + + { /*28*//*29*/} +
    + /*5*//*6*//*7*//*18*/ ); }; const render2 = () => { return ( - // Prop comment. - // Child string comment. -
    - hello -
    +
    + {// Child string comment. + 'hello'} +
    ); }; const render3 = () => { return ( - // Child element comment.
    - -
    + {// Child element comment. + } +
    ); }; const render4 = () => { - return ( - /* No props to see here! */ - - ); + return /* No props to see here! */; }; diff --git a/codemods/create-element-to-jsx/tests/spread-prop-comments/expected.tsx b/codemods/create-element-to-jsx/tests/spread-prop-comments/expected.tsx new file mode 100644 index 0000000..3a8cec3 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/spread-prop-comments/expected.tsx @@ -0,0 +1,4 @@ +var React = require('react'); +
    + {/*B*/'x'} +
    ; diff --git a/codemods/create-element-to-jsx/tests/spread-prop-comments/input.tsx b/codemods/create-element-to-jsx/tests/spread-prop-comments/input.tsx new file mode 100644 index 0000000..95b2b96 --- /dev/null +++ b/codemods/create-element-to-jsx/tests/spread-prop-comments/input.tsx @@ -0,0 +1,2 @@ +var React = require('react'); +React.createElement('div', /*A*/getProps()/*B*/, 'x'); diff --git a/codemods/create-element-to-jsx/tests/spread-prop-comments/metrics.json b/codemods/create-element-to-jsx/tests/spread-prop-comments/metrics.json new file mode 100644 index 0000000..e5b890a --- /dev/null +++ b/codemods/create-element-to-jsx/tests/spread-prop-comments/metrics.json @@ -0,0 +1,10 @@ +{ + "create-element-to-jsx-conversions": [ + { + "cardinality": { + "file": "tests/spread-prop-comments/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file From 18482f27dc78712a340c98e34037b8f784587569 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Tue, 14 Apr 2026 22:30:56 +0200 Subject: [PATCH 07/29] fix pure-component --- codemods/pure-component/package.json | 2 +- codemods/pure-component/scripts/codemod.ts | 613 +++++++++++++----- .../scripts/differential-tests.mjs | 80 +++ .../pure-component/scripts/warning-tests.mjs | 83 +++ .../pure-component/tests/arrow/expected.tsx | 24 +- .../tests/destructuring/expected.tsx | 59 +- .../tests/export-default-arrow/expected.tsx | 4 + .../tests/export-default-arrow/input.tsx | 5 + .../tests/export-default-arrow/metrics.json | 10 + .../export-default-arrow/test.config.json | 5 + .../export-default-function/expected.tsx | 3 + .../tests/export-default-function/input.tsx | 5 + .../export-default-function/metrics.json | 10 + .../tests/mixed-superclasses/expected.tsx | 7 + .../tests/mixed-superclasses/input.tsx | 9 + .../tests/mixed-superclasses/metrics.json | 10 + .../tests/named-export-arrow/expected.tsx | 3 + .../tests/named-export-arrow/input.tsx | 5 + .../tests/named-export-arrow/metrics.json | 10 + .../tests/named-export-arrow/test.config.json | 5 + .../tests/named-export/expected.tsx | 3 + .../tests/named-export/input.tsx | 5 + .../tests/named-export/metrics.json | 10 + .../tests/named-import-component/expected.tsx | 3 + .../tests/named-import-component/input.tsx | 5 + .../tests/named-import-component/metrics.json | 10 + .../expected.tsx | 3 + .../input.tsx | 5 + .../metrics.json | 10 + .../react-native-named-import/expected.tsx | 3 + .../tests/react-native-named-import/input.tsx | 5 + .../react-native-named-import/metrics.json | 10 + 32 files changed, 825 insertions(+), 199 deletions(-) create mode 100644 codemods/pure-component/scripts/differential-tests.mjs create mode 100644 codemods/pure-component/scripts/warning-tests.mjs create mode 100644 codemods/pure-component/tests/export-default-arrow/expected.tsx create mode 100644 codemods/pure-component/tests/export-default-arrow/input.tsx create mode 100644 codemods/pure-component/tests/export-default-arrow/metrics.json create mode 100644 codemods/pure-component/tests/export-default-arrow/test.config.json create mode 100644 codemods/pure-component/tests/export-default-function/expected.tsx create mode 100644 codemods/pure-component/tests/export-default-function/input.tsx create mode 100644 codemods/pure-component/tests/export-default-function/metrics.json create mode 100644 codemods/pure-component/tests/mixed-superclasses/expected.tsx create mode 100644 codemods/pure-component/tests/mixed-superclasses/input.tsx create mode 100644 codemods/pure-component/tests/mixed-superclasses/metrics.json create mode 100644 codemods/pure-component/tests/named-export-arrow/expected.tsx create mode 100644 codemods/pure-component/tests/named-export-arrow/input.tsx create mode 100644 codemods/pure-component/tests/named-export-arrow/metrics.json create mode 100644 codemods/pure-component/tests/named-export-arrow/test.config.json create mode 100644 codemods/pure-component/tests/named-export/expected.tsx create mode 100644 codemods/pure-component/tests/named-export/input.tsx create mode 100644 codemods/pure-component/tests/named-export/metrics.json create mode 100644 codemods/pure-component/tests/named-import-component/expected.tsx create mode 100644 codemods/pure-component/tests/named-import-component/input.tsx create mode 100644 codemods/pure-component/tests/named-import-component/metrics.json create mode 100644 codemods/pure-component/tests/named-import-purecomponent-alias/expected.tsx create mode 100644 codemods/pure-component/tests/named-import-purecomponent-alias/input.tsx create mode 100644 codemods/pure-component/tests/named-import-purecomponent-alias/metrics.json create mode 100644 codemods/pure-component/tests/react-native-named-import/expected.tsx create mode 100644 codemods/pure-component/tests/react-native-named-import/input.tsx create mode 100644 codemods/pure-component/tests/react-native-named-import/metrics.json diff --git a/codemods/pure-component/package.json b/codemods/pure-component/package.json index 0db6bf0..c18d5a9 100644 --- a/codemods/pure-component/package.json +++ b/codemods/pure-component/package.json @@ -4,7 +4,7 @@ "description": "Convert simple React classes into pure function components", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/warning-tests.mjs ./scripts/differential-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/pure-component/scripts/codemod.ts b/codemods/pure-component/scripts/codemod.ts index 72db390..8a06b02 100644 --- a/codemods/pure-component/scripts/codemod.ts +++ b/codemods/pure-component/scripts/codemod.ts @@ -2,38 +2,111 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +type ReactImportAliases = { + component: Set; + pureComponent: Set; + removableNamed: Set; +}; + +type ReactSuperclassKind = "Component" | "PureComponent"; + +const REACT_SOURCES = new Set(["react", "React", "react/addons", "react-native"]); + +function truthyParam(value: unknown): boolean { + return value === true || value === "true"; +} + function metricFile(filename: string): string { const cwd = process.cwd() + "/"; return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; } function namedChildren(node: SgNode | null): SgNode[] { - if (!node) { - return []; + if (!node) return []; + return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function sourceText(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = node.text(); + return text.length >= 2 ? text.slice(1, -1) : null; +} + +function importSource(importStmt: SgNode): SgNode | null { + return importStmt.field("source") ?? importStmt.find({ rule: { kind: "string" } }); +} + +function isReactSource(importStmt: SgNode): boolean { + const source = importSource(importStmt); + const value = source ? sourceText(source) : null; + return value !== null && REACT_SOURCES.has(value); +} + +function reactImportAliases(rootNode: SgNode): ReactImportAliases { + const component = new Set(); + const pureComponent = new Set(); + const removableNamed = new Set(); + + for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (!isReactSource(importStmt)) continue; + + for (const specifier of importStmt.findAll({ rule: { kind: "import_specifier" } })) { + const imported = specifier.field("name")?.text(); + const aliasNode = specifier.field("alias"); + const alias = aliasNode?.text() ?? imported; + if (!imported || !alias) continue; + + if (imported === "Component") { + component.add(alias); + } else if (imported === "PureComponent") { + pureComponent.add(alias); + } else { + continue; + } + + // Legacy removes an import specifier by searching for its `imported` name + // in the rest of the file. For `{ Component as C }`, imported=Component but + // the class extends `C`; the search finds no `Component` identifier and the + // specifier gets kept (legacy quirk). Mirror that: only flag as removable + // when the local name equals the imported name. + if (!aliasNode) removableNamed.add(alias); + } } - return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); + return { component, pureComponent, removableNamed }; } -function isReactComponentClass(classDecl: SgNode): boolean { +function reactSuperclassKind( + classDecl: SgNode, + aliases: ReactImportAliases, +): ReactSuperclassKind | null { const heritage = classDecl.find({ rule: { kind: "class_heritage" } }); - if (!heritage) { - return false; + if (!heritage) return null; + + const directExtends = heritage.find({ + rule: { any: [{ kind: "member_expression" }, { kind: "identifier" }] }, + }); + if (!directExtends) return null; + + if (directExtends.kind() === "member_expression") { + const object = directExtends.field("object"); + const property = directExtends.field("property")?.text() ?? ""; + if (object?.text() !== "React") return null; + if (property === "Component" || property === "PureComponent") return property; + return null; } - return heritage.findAll({ rule: { kind: "member_expression" } }) - .some((member) => { - const object = member.field("object"); - const property = member.field("property"); - return object?.text() === "React" && - (property?.text() === "Component" || property?.text() === "PureComponent"); - }); + const name = directExtends.text(); + if (aliases.component.has(name)) return "Component"; + if (aliases.pureComponent.has(name)) return "PureComponent"; + return null; } function classBodyMembers(classDecl: SgNode): SgNode[] { const body = classDecl.find({ rule: { kind: "class_body" } }); - return namedChildren(body).filter((member) => - member.kind() === "method_definition" || member.kind() === "public_field_definition" + return namedChildren(body).filter( + (m) => m.kind() === "method_definition" || m.kind() === "public_field_definition", ); } @@ -41,139 +114,275 @@ function memberName(member: SgNode): string { return member.field("name")?.text() ?? ""; } +function isRenderMethod(member: SgNode): boolean { + return member.kind() === "method_definition" && memberName(member) === "render"; +} + function isStaticField(member: SgNode): boolean { - return member.kind() === "public_field_definition" && member.text().startsWith("static "); + if (member.kind() !== "public_field_definition") return false; + return member.children().some((child) => child.kind() === "static" || child.text() === "static"); } function isPropsTypeField(member: SgNode): boolean { return member.kind() === "public_field_definition" && - !member.text().startsWith("static ") && + !isStaticField(member) && memberName(member) === "props"; } +function onlyHasSafeMembers(classDecl: SgNode): boolean { + const members = classBodyMembers(classDecl); + const methods = members.filter((m) => m.kind() === "method_definition"); + if (methods.length !== 1 || !isRenderMethod(methods[0]!)) return false; + return members.every((m) => isRenderMethod(m) || isPropsTypeField(m) || isStaticField(m)); +} + function hasRefAttribute(classDecl: SgNode): boolean { - return classDecl.findAll({ - rule: { - kind: "jsx_attribute", - }, - }).some((attr) => namedChildren(attr)[0]?.text() === "ref"); + return classDecl.findAll({ rule: { kind: "jsx_attribute" } }) + .some((attr) => namedChildren(attr)[0]?.text() === "ref"); } function renderMethod(classDecl: SgNode): SgNode | null { - return classBodyMembers(classDecl).find((member) => - member.kind() === "method_definition" && memberName(member) === "render" - ) ?? null; + return classBodyMembers(classDecl).find(isRenderMethod) ?? null; } -function propsTypeAnnotation(classDecl: SgNode): string { - const propsField = classBodyMembers(classDecl).find(isPropsTypeField); - const annotation = propsField?.find({ rule: { kind: "type_annotation" } }); +function propsField(classDecl: SgNode): SgNode | null { + return classBodyMembers(classDecl).find(isPropsTypeField) ?? null; +} + +function propsTypeAnnotationText(classDecl: SgNode): string { + const annotation = propsField(classDecl)?.find({ rule: { kind: "type_annotation" } }); return annotation?.text() ?? ""; } -function isThisProps(node: SgNode): boolean { - if (node.kind() !== "member_expression") { - return false; - } +function propsObjectType(classDecl: SgNode): SgNode | null { + const annotation = propsField(classDecl)?.find({ rule: { kind: "type_annotation" } }); + const typeNode = namedChildren(annotation)[0] ?? null; + return typeNode?.kind() === "object_type" ? typeNode : null; +} +function isThisProps(node: SgNode | null | undefined): boolean { + if (!node || node.kind() !== "member_expression") return false; const object = node.field("object"); const property = node.field("property"); return object?.kind() === "this" && property?.text() === "props"; } -function propsAccesses(body: SgNode): SgNode[] { - return body.findAll({ rule: { kind: "member_expression" } }) - .filter((member) => isThisProps(member.field("object"))); +function thisPropsExpressions(body: SgNode): SgNode[] { + return body.findAll({ rule: { kind: "member_expression" } }).filter(isThisProps); } -function bareThisPropsUses(body: SgNode): SgNode[] { +function propsAccesses(body: SgNode): SgNode[] { return body.findAll({ rule: { kind: "member_expression" } }) - .filter((member) => { - if (!isThisProps(member)) { - return false; - } + .filter((m) => isThisProps(m.field("object"))); +} - const parent = member.parent(); - return !(parent?.kind() === "member_expression" && parent.field("object")?.id() === member.id()); - }); +function barePropsIdentifiers(methodDef: SgNode): boolean { + return methodDef.findAll({ + rule: { + any: [ + { kind: "identifier", regex: "^props$" }, + { kind: "property_identifier", regex: "^props$" }, + ], + }, + }).some((identifier) => { + const parent = identifier.parent(); + const grand = parent?.parent(); + if ( + parent?.kind() === "member_expression" && + grand?.kind() === "member_expression" && + grand.field("object")?.id() === parent.id() + ) { + return false; + } + return true; + }); } -function safeDuplicateDeclarators(body: SgNode): Map> { +function duplicateDeclarators(body: SgNode): Map> { const duplicates = new Map>(); - for (const declarator of body.findAll({ rule: { kind: "variable_declarator" } })) { const name = declarator.field("name"); const value = declarator.field("value"); - if (name?.kind() !== "identifier" || value?.kind() !== "member_expression") { - continue; - } - + if (name?.kind() !== "identifier" || value?.kind() !== "member_expression") continue; const object = value.field("object"); const property = value.field("property"); - if (!isThisProps(object) || property?.text() !== name.text()) { - continue; - } - + if (!object || !property || !isThisProps(object) || property.text() !== name.text()) continue; duplicates.set(name.text(), declarator); } - return duplicates; } -function propNames(body: SgNode): string[] { - return Array.from(new Set( - propsAccesses(body) - .map((access) => access.field("property")?.text() ?? "") - .filter((name) => name.length > 0), - )).sort(); +function propNamesInOrder(body: SgNode): string[] { + const names: string[] = []; + const seen = new Set(); + for (const access of propsAccesses(body)) { + const property = access.field("property")?.text(); + if (property && !seen.has(property)) { + seen.add(property); + names.push(property); + } + } + return names; } -function hasDestructureConflicts(body: SgNode, names: string[], duplicates: Map>): boolean { - const propNameSet = new Set(names); - const safeNames = new Set(duplicates.keys()); +function hasAssignmentNamesShadowing( + body: SgNode, + propNames: string[], + duplicates: Map>, +): boolean { + const propNameSet = new Set(propNames); + const duplicateNames = new Set(duplicates.keys()); return body.findAll({ rule: { kind: "identifier" } }).some((identifier) => { const name = identifier.text(); - if (!propNameSet.has(name) || safeNames.has(name)) { + if (!propNameSet.has(name) || duplicateNames.has(name)) return false; + const parent = identifier.parent(); + if (!parent) return true; + if (parent.kind() === "member_expression" && parent.field("property")?.id() === identifier.id()) { return false; } + const object = parent.kind() === "member_expression" ? parent.field("object") : null; + if (object && isThisProps(object)) return false; + return true; + }); +} - const parent = identifier.parent(); - if (!parent) { - return true; +function canDestructure( + classDecl: SgNode, + methodDef: SgNode, + body: SgNode, +): boolean { + const names = propNamesInOrder(body); + const duplicates = duplicateDeclarators(body); + if (barePropsIdentifiers(methodDef) || hasAssignmentNamesShadowing(body, names, duplicates)) { + return false; + } + // Without an inline object type we can't produce a typed destructure pattern. + // The legacy transform crashes in this case (calls `.properties.forEach` on a + // non-object type); we skip safely and fall back to `props: Annotation`. + const annotationText = propsTypeAnnotationText(classDecl); + if (annotationText === "") return true; + return propsObjectType(classDecl) !== null; +} + +function lineDeletionRange( + node: SgNode, + source: string, +): { start: number; end: number } { + const range = node.range(); + let start = range.start.index; + while (start > 0 && source[start - 1] !== "\n") start--; + let end = range.end.index; + if (end < source.length && source[end] === "\n") end++; + return { start, end }; +} + +function rangeContains( + outer: { start: number; end: number }, + inner: { start: number; end: number }, +): boolean { + return inner.start >= outer.start && inner.end <= outer.end; +} + +function rewriteRenderBody( + renderBody: SgNode, + source: string, + mayDestructure: boolean, + duplicates: Map>, +): string { + const edits: Edit[] = []; + const deletedRanges: Array<{ start: number; end: number }> = []; + + if (mayDestructure) { + for (const declarator of duplicates.values()) { + const stmt = declarator.parent(); + if (!stmt) continue; + if (stmt.kind() !== "lexical_declaration" && stmt.kind() !== "variable_declaration") continue; + const declarators = stmt.findAll({ rule: { kind: "variable_declarator" } }); + if (declarators.length !== 1) continue; + const range = lineDeletionRange(stmt, source); + deletedRanges.push(range); + edits.push({ startPos: range.start, endPos: range.end, insertedText: "" }); } + } - if (parent.kind() === "member_expression" && parent.field("property")?.id() === identifier.id()) { - return false; + const inDeleted = (node: SgNode) => { + const r = node.range(); + const span = { start: r.start.index, end: r.end.index }; + return deletedRanges.some((dr) => rangeContains(dr, span)); + }; + + if (mayDestructure) { + for (const access of propsAccesses(renderBody)) { + if (inDeleted(access)) continue; + const prop = access.field("property")?.text(); + if (prop) edits.push(access.replace(prop)); + } + } else { + for (const me of thisPropsExpressions(renderBody)) { + edits.push(me.replace("props")); } + } - const object = parent.kind() === "member_expression" ? parent.field("object") : null; - if (object && isThisProps(object)) { - return false; + return renderBody.commitEdits(edits); +} + +function normalizeBlock(blockText: string, indent: string): string { + const lines = blockText.split("\n"); + if (lines.length <= 2) return blockText; + + const inner = lines.slice(1, -1); + const nonEmpty = inner.filter((line) => line.trim().length > 0); + const commonIndent = nonEmpty.length === 0 + ? 0 + : Math.min(...nonEmpty.map((line) => line.match(/^ */)?.[0].length ?? 0)); + const body = inner + .map((line) => (line.length >= commonIndent ? line.slice(commonIndent) : line)) + .map((line) => (line.trim().length > 0 ? `${indent}${line}` : "")) + .join("\n"); + return `{\n${body}\n}`; +} + +function renderDestructuredParam(names: string[], objectType: SgNode | null): string { + const typed = new Map(); + if (objectType) { + for (const prop of objectType.findAll({ rule: { kind: "property_signature" } })) { + const key = prop.field("name")?.text(); + const type = prop.find({ rule: { kind: "type_annotation" } })?.text().replace(/^:\s*/, ""); + if (key && type) typed.set(key, type); } + } - return true; - }); + const lines = names.map((name) => + typed.has(name) ? ` ${name}: ${typed.get(name)},` : ` ${name},`, + ); + return `(\n {\n${lines.join("\n")}\n },\n)`; } -function buildParamText( - names: string[], - annotation: string, - destructuring: boolean, +function renderFunctionLike( + name: string, + bodyText: string, + paramText: string, + useArrows: boolean, ): string { - if (names.length === 0 && annotation.length === 0) { - return ""; + const body = normalizeBlock(bodyText, " "); + + if (useArrows) { + let left: string; + if (paramText === "") left = "()"; + else if (paramText === "props") left = "props"; + else if (paramText.startsWith("(")) left = paramText; + else left = `(${paramText})`; + return `const ${name} = ${left} => ${body};`; } - if (!destructuring) { - return `props${annotation}`; + if (paramText.startsWith("(")) { + return `function ${name}${paramText} ${body}`; } - - const pattern = `{ ${names.join(", ")} }`; - return annotation.length > 0 ? `${pattern}${annotation}` : pattern; + return `function ${name}(${paramText}) ${body}`; } -function buildStaticAssignments(className: string, classDecl: SgNode): string[] { +function renderStaticAssignments(className: string, classDecl: SgNode): string[] { return classBodyMembers(classDecl) .filter(isStaticField) .map((field) => { @@ -183,113 +392,191 @@ function buildStaticAssignments(className: string, classDecl: SgNode): stri }); } -function transformBody( - renderBody: SgNode, - destructure: boolean, - names: string[], - duplicates: Map>, -): string { - const edits: Edit[] = []; - - for (const access of propsAccesses(renderBody)) { - const propName = access.field("property")?.text(); - if (!propName) { - continue; - } +function getClassName(classDecl: SgNode): string | null { + return classDecl.field("name")?.text() ?? null; +} - edits.push(access.replace(destructure ? propName : `props.${propName}`)); - } +function skipWarning(classDecl: SgNode): string { + const name = getClassName(classDecl) ?? "Unknown"; + const filename = classDecl.getRoot().filename(); + const { line, column } = classDecl.range().start; + // Jscodeshift reports line as 1-indexed, column as 0-indexed. Ast-grep is + // 0-indexed on both; +1 on the line matches legacy output exactly. + return `Class "${name}" skipped in ${filename} on ${line + 1}:${column}`; +} - for (const use of bareThisPropsUses(renderBody)) { - edits.push(use.replace("props")); - } +function enclosingExportStatement(classDecl: SgNode): SgNode | null { + const parent = classDecl.parent(); + return parent?.kind() === "export_statement" ? parent : null; +} - if (destructure) { - for (const declarator of duplicates.values()) { - const statement = declarator.ancestors().find((ancestor) => - ancestor.kind() === "lexical_declaration" || ancestor.kind() === "variable_declaration" - ); - if (statement) { - edits.push({ - startPos: statement.range().start.index, - endPos: statement.range().end.index, - insertedText: "", - }); - } - } - } +function isDefaultExportStatement(exportStmt: SgNode): boolean { + return exportStmt.children().some((c) => c.kind() === "default" || c.text() === "default"); +} - return renderBody.commitEdits(edits); +function remainingNamedImportUse( + rootNode: SgNode, + alias: string, + transformedClassIds: Set, +): boolean { + return rootNode + .findAll({ rule: { kind: "identifier", regex: `^${alias}$` } }) + .some((identifier) => { + if (identifier.ancestors().some((a) => a.kind() === "import_statement")) return false; + const classDecl = identifier.ancestors().find((a) => a.kind() === "class_declaration"); + if (!classDecl) return true; + return !transformedClassIds.has(classDecl.id()); + }); } -function isPureClass(classDecl: SgNode): boolean { - if (!isReactComponentClass(classDecl) || hasRefAttribute(classDecl)) { - return false; - } +function buildImportSpecifierEdits( + importStmt: SgNode, + aliasesToDrop: Set, + source: string, +): Edit[] { + const named = importStmt.find({ rule: { kind: "named_imports" } }); + if (!named) return []; + + const specifiers = named.findAll({ rule: { kind: "import_specifier" } }); + const toKeep = specifiers.filter((s) => { + const local = s.field("alias")?.text() ?? s.field("name")?.text() ?? ""; + return !aliasesToDrop.has(local); + }); - const members = classBodyMembers(classDecl); - const methods = members.filter((member) => member.kind() === "method_definition"); - if (methods.length !== 1 || memberName(methods[0]!) !== "render") { - return false; + if (toKeep.length === specifiers.length) return []; + + const clause = importStmt.find({ rule: { kind: "import_clause" } }); + const defaultImport = clause + ? namedChildren(clause).find((c) => c.kind() === "identifier") + : null; + + if (toKeep.length === 0) { + if (defaultImport) { + return [{ + startPos: clause!.range().start.index, + endPos: clause!.range().end.index, + insertedText: defaultImport.text(), + }]; + } + // Legacy uses ImportSpecifier.remove() which drops only the specifier and + // leaves `import 'source';` as a side-effect import. Match that by + // replacing the import_clause (plus the trailing `from`) with nothing. + const sourceNode = importSource(importStmt); + if (!clause || !sourceNode) { + const range = lineDeletionRange(importStmt, source); + return [{ startPos: range.start, endPos: range.end, insertedText: "" }]; + } + return [{ + startPos: clause.range().start.index, + endPos: sourceNode.range().start.index, + insertedText: "", + }]; } - return members.every((member) => member.kind() === "method_definition" || isStaticField(member) || isPropsTypeField(member)); + const newSpecifiers = toKeep.map((s) => s.text()).join(", "); + return [{ + startPos: named.range().start.index, + endPos: named.range().end.index, + insertedText: `{ ${newSpecifiers} }`, + }]; } const transform: Transform = async (root, options) => { const rootNode = root.root(); - const edits: Edit[] = []; + const source = rootNode.text(); const metric = useMetricAtom("pure-component-conversions"); - const useArrows = options.params?.useArrows === true || options.params?.useArrows === "true"; - const destructuring = options.params?.destructuring === true || options.params?.destructuring === "true"; - let conversions = 0; + const useArrows = truthyParam(options.params?.useArrows); + const destructuring = truthyParam(options.params?.destructuring); + const silenceWarnings = truthyParam(options.params?.silenceWarnings); - for (const classDecl of rootNode.findAll({ rule: { kind: "class_declaration" } })) { - if (!isPureClass(classDecl)) { + const aliases = reactImportAliases(rootNode); + const edits: Edit[] = []; + const transformedClassIds = new Set(); + const reactClasses = rootNode.findAll({ rule: { kind: "class_declaration" } }) + .map((classDecl) => ({ classDecl, kind: reactSuperclassKind(classDecl, aliases) })) + .filter((entry): entry is { classDecl: SgNode; kind: ReactSuperclassKind } => entry.kind !== null); + const targetSuperclassKind = reactClasses.some((entry) => entry.kind === "Component") + ? "Component" + : reactClasses.some((entry) => entry.kind === "PureComponent") + ? "PureComponent" + : null; + + for (const { classDecl, kind } of reactClasses) { + if (targetSuperclassKind === null || kind !== targetSuperclassKind) continue; + + if (!onlyHasSafeMembers(classDecl) || hasRefAttribute(classDecl)) { + if (!silenceWarnings) console.warn(skipWarning(classDecl)); continue; } - const className = classDecl.field("name")?.text(); + const name = getClassName(classDecl); const render = renderMethod(classDecl); const renderBody = render?.field("body"); - if (!className || !renderBody) { - continue; + if (!name || !render || !renderBody) continue; + + const names = propNamesInOrder(renderBody); + const duplicates = duplicateDeclarators(renderBody); + const objectType = propsObjectType(classDecl); + const annotationText = propsTypeAnnotationText(classDecl); + const hasPropsUse = thisPropsExpressions(renderBody).length > 0; + const canDestr = canDestructure(classDecl, render, renderBody); + const mayDestructure = destructuring && hasPropsUse && canDestr; + + if (destructuring && !canDestr) { + console.warn(`Unable to destructure ${name} props.`); } - const annotation = propsTypeAnnotation(classDecl); - const names = propNames(renderBody); - const duplicates = safeDuplicateDeclarators(renderBody); - const canDestructure = destructuring && - bareThisPropsUses(renderBody).length === 0 && - !hasDestructureConflicts(renderBody, names, duplicates); - const bodyText = transformBody(renderBody, canDestructure, names, duplicates); - const paramText = buildParamText(names, annotation, canDestructure); - const staticAssignments = buildStaticAssignments(className, classDecl); - - let replacement = ""; - if (useArrows) { - replacement = `const ${className} = (${paramText}) => ${bodyText};`; - } else { - replacement = `function ${className}(${paramText}) ${bodyText}`; + const newBodyText = rewriteRenderBody(renderBody, source, mayDestructure, duplicates); + + let paramText = ""; + if (hasPropsUse) { + if (mayDestructure) { + paramText = renderDestructuredParam(names, objectType); + } else { + paramText = annotationText ? `props${annotationText}` : "props"; + } } - if (staticAssignments.length > 0) { - replacement += `\n\n${staticAssignments.join("\n")}`; + let replacement = renderFunctionLike(name, newBodyText, paramText, useArrows); + const statics = renderStaticAssignments(name, classDecl); + if (statics.length > 0) replacement += `\n\n${statics.join("\n")}`; + + const exportStmt = enclosingExportStatement(classDecl); + const isDefault = exportStmt ? isDefaultExportStatement(exportStmt) : false; + const splitDefault = isDefault && useArrows; + + let editStart: number; + let editEnd: number; + if (splitDefault) { + editStart = exportStmt!.range().start.index; + editEnd = exportStmt!.range().end.index; + replacement = `${replacement}\nexport default ${name};`; + } else { + editStart = classDecl.range().start.index; + editEnd = classDecl.range().end.index; } - edits.push({ - startPos: classDecl.range().start.index, - endPos: classDecl.range().end.index, - insertedText: replacement, - }); - conversions++; + edits.push({ startPos: editStart, endPos: editEnd, insertedText: replacement }); + transformedClassIds.add(classDecl.id()); } - if (edits.length === 0) { - return null; + if (edits.length === 0) return null; + + const aliasesToDrop = new Set(); + for (const alias of aliases.removableNamed) { + if (!remainingNamedImportUse(rootNode, alias, transformedClassIds)) { + aliasesToDrop.add(alias); + } + } + + if (aliasesToDrop.size > 0) { + for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (!isReactSource(importStmt)) continue; + edits.push(...buildImportSpecifierEdits(importStmt, aliasesToDrop, source)); + } } - metric.increment({ file: metricFile(root.filename()) }, conversions); + metric.increment({ file: metricFile(root.filename()) }, transformedClassIds.size); return rootNode.commitEdits(edits); }; diff --git a/codemods/pure-component/scripts/differential-tests.mjs b/codemods/pure-component/scripts/differential-tests.mjs new file mode 100644 index 0000000..c1ec390 --- /dev/null +++ b/codemods/pure-component/scripts/differential-tests.mjs @@ -0,0 +1,80 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); +const packageDir = path.dirname(path.dirname(__filename)); +const codemodFile = path.join(packageDir, "scripts/codemod.ts"); +const testsDir = path.join(packageDir, "tests"); + +function readFixture(name, file) { + return readFileSync(path.join(testsDir, name, file), "utf8").trim(); +} + +function readParams(name) { + const configPath = path.join(testsDir, name, "test.config.json"); + try { + const config = JSON.parse(readFileSync(configPath, "utf8")); + return config.params ?? {}; + } catch { + return {}; + } +} + +function runJssgOutput(input, params = {}) { + const root = mkdtempSync(path.join(tmpdir(), "pure-component-jssg-")); + const caseDir = path.join(root, "case"); + mkdirSync(caseDir); + writeFileSync(path.join(caseDir, "input.tsx"), input, "utf8"); + writeFileSync(path.join(caseDir, "expected.tsx"), input, "utf8"); + if (Object.keys(params).length > 0) { + writeFileSync(path.join(caseDir, "test.config.json"), JSON.stringify({ params }, null, 2)); + } + + try { + const result = spawnSync( + "pnpm", + [ + "dlx", + "codemod@latest", + "jssg", + "test", + "-l", + "tsx", + codemodFile, + root, + "-u", + ], + { cwd: packageDir, encoding: "utf8", timeout: 20000 }, + ); + assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); + return readFileSync(path.join(caseDir, "expected.tsx"), "utf8").trim(); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +const parityCases = [ + "named-import-component", + "named-import-purecomponent-alias", + "export-default-arrow", + "export-default-function", + "named-export", + "named-export-arrow", + "react-native-named-import", + "mixed-superclasses", +]; + +for (const name of parityCases) { + test(`matches checked-in parity fixture for ${name}`, () => { + const input = readFixture(name, "input.tsx"); + const expected = readFixture(name, "expected.tsx"); + const params = readParams(name); + const jssgOutput = runJssgOutput(input, params); + assert.strictEqual(jssgOutput, expected); + }); +} diff --git a/codemods/pure-component/scripts/warning-tests.mjs b/codemods/pure-component/scripts/warning-tests.mjs new file mode 100644 index 0000000..9940a0f --- /dev/null +++ b/codemods/pure-component/scripts/warning-tests.mjs @@ -0,0 +1,83 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); +const packageDir = path.dirname(path.dirname(__filename)); +const codemodFile = path.join(packageDir, "scripts/codemod.ts"); + +function runFixtureTest(input, params = {}) { + const root = mkdtempSync(path.join(tmpdir(), "pure-component-warn-")); + const caseDir = path.join(root, "case"); + mkdirSync(caseDir); + writeFileSync(path.join(caseDir, "input.tsx"), input, "utf8"); + writeFileSync(path.join(caseDir, "expected.tsx"), input, "utf8"); + if (Object.keys(params).length > 0) { + writeFileSync(path.join(caseDir, "test.config.json"), JSON.stringify({ params }, null, 2)); + } + + try { + return spawnSync( + "pnpm", + [ + "dlx", + "codemod@latest", + "jssg", + "test", + "-l", + "tsx", + codemodFile, + root, + "--strictness", + "loose", + ], + { + cwd: packageDir, + encoding: "utf8", + timeout: 20000, + }, + ); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +test("warns when an impure class is skipped", () => { + const result = runFixtureTest(`import React from 'React'; +class Impure extends React.Component { + componentWillMount() {} + render() { return
    ; } +} +`); + const combined = `${result.stdout}\n${result.stderr}`; + assert.match(combined, /Class "Impure" skipped in/); +}); + +test("suppresses skip warnings with silenceWarnings", () => { + const result = runFixtureTest(`import React from 'React'; +class Impure extends React.Component { + componentWillMount() {} + render() { return
    ; } +} +`, { silenceWarnings: true }); + const combined = `${result.stdout}\n${result.stderr}`; + assert.doesNotMatch(combined, /Class "Impure" skipped in/); +}); + +test("warns when destructuring cannot be applied", () => { + const result = runFixtureTest(`import React from 'React'; +function doSomething(props) { return props; } +class UsesThisDotProps extends React.Component { + render() { + doSomething(this.props); + return
    ; + } +} +`, { destructuring: true }); + const combined = `${result.stdout}\n${result.stderr}`; + assert.match(combined, /Unable to destructure UsesThisDotProps props\./); +}); diff --git a/codemods/pure-component/tests/arrow/expected.tsx b/codemods/pure-component/tests/arrow/expected.tsx index 34bf613..a155191 100644 --- a/codemods/pure-component/tests/arrow/expected.tsx +++ b/codemods/pure-component/tests/arrow/expected.tsx @@ -6,9 +6,9 @@ function render() { return
    ; } -const Pure = (props) => { - return
    ; - }; +const Pure = props => { + return
    ; +}; class Impure extends React.Component { componentWillMount() { @@ -20,22 +20,22 @@ class Impure extends React.Component { } const PureWithoutProps = () => { - return
    ; - }; + return
    ; +}; const PureWithTypes = (props: { foo: string }) => { - return
    ; - }; + return
    ; +}; type Props = { foo: string }; const PureWithTypes2 = (props: Props) => { - return
    ; - }; + return
    ; +}; -const PureWithPropTypes = (props) => { - return
    {props.foo}
    ; - }; +const PureWithPropTypes = props => { + return
    {props.foo}
    ; +}; PureWithPropTypes.propTypes = { foo: React.PropTypes.string }; diff --git a/codemods/pure-component/tests/destructuring/expected.tsx b/codemods/pure-component/tests/destructuring/expected.tsx index 2075c82..714b15c 100644 --- a/codemods/pure-component/tests/destructuring/expected.tsx +++ b/codemods/pure-component/tests/destructuring/expected.tsx @@ -6,32 +6,45 @@ const shadow = 'shadow'; function doSomething(props) { return props; } -function ShouldDestsructure({ foo }) { - return
    ; - } - -function ShouldDestructureAndRemoveDuplicateDeclaration({ bar, bizzaz, foo }) { - const fizz = { buzz: 'buzz' }; - - const baz = bizzaz; - const buzz = fizz.buzz; - return
    ; - } +function ShouldDestsructure( + { + foo, + }, +) { + return
    ; +} + +function ShouldDestructureAndRemoveDuplicateDeclaration( + { + bar, + bizzaz, + foo, + }, +) { + const fizz = { buzz: 'buzz' }; + const baz = bizzaz; + const buzz = fizz.buzz; + return
    ; +} function UsesThisDotProps(props) { - doSomething(props); - return
    ; - } + doSomething(props); + return
    ; +} function DestructuresThisDotProps(props) { - const { bar } = props; - return
    ; - } + const { bar } = props; + return
    ; +} function HasShadowProps(props) { - return
    ; - } - -function PureWithTypes({ foo }: { foo: string }) { - return
    ; - } + return
    ; +} + +function PureWithTypes( + { + foo: string, + }, +) { + return
    ; +} diff --git a/codemods/pure-component/tests/export-default-arrow/expected.tsx b/codemods/pure-component/tests/export-default-arrow/expected.tsx new file mode 100644 index 0000000..d8fe639 --- /dev/null +++ b/codemods/pure-component/tests/export-default-arrow/expected.tsx @@ -0,0 +1,4 @@ +import React from 'React'; + +const Pure = () => { return
    ; }; +export default Pure; diff --git a/codemods/pure-component/tests/export-default-arrow/input.tsx b/codemods/pure-component/tests/export-default-arrow/input.tsx new file mode 100644 index 0000000..0f4bd1f --- /dev/null +++ b/codemods/pure-component/tests/export-default-arrow/input.tsx @@ -0,0 +1,5 @@ +import React from 'React'; + +export default class Pure extends React.Component { + render() { return
    ; } +} diff --git a/codemods/pure-component/tests/export-default-arrow/metrics.json b/codemods/pure-component/tests/export-default-arrow/metrics.json new file mode 100644 index 0000000..93360c5 --- /dev/null +++ b/codemods/pure-component/tests/export-default-arrow/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/export-default-arrow/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/export-default-arrow/test.config.json b/codemods/pure-component/tests/export-default-arrow/test.config.json new file mode 100644 index 0000000..8646dd5 --- /dev/null +++ b/codemods/pure-component/tests/export-default-arrow/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "useArrows": true + } +} diff --git a/codemods/pure-component/tests/export-default-function/expected.tsx b/codemods/pure-component/tests/export-default-function/expected.tsx new file mode 100644 index 0000000..b87749b --- /dev/null +++ b/codemods/pure-component/tests/export-default-function/expected.tsx @@ -0,0 +1,3 @@ +import React from 'React'; + +export default function Pure() { return
    ; } diff --git a/codemods/pure-component/tests/export-default-function/input.tsx b/codemods/pure-component/tests/export-default-function/input.tsx new file mode 100644 index 0000000..0f4bd1f --- /dev/null +++ b/codemods/pure-component/tests/export-default-function/input.tsx @@ -0,0 +1,5 @@ +import React from 'React'; + +export default class Pure extends React.Component { + render() { return
    ; } +} diff --git a/codemods/pure-component/tests/export-default-function/metrics.json b/codemods/pure-component/tests/export-default-function/metrics.json new file mode 100644 index 0000000..5bcf0ec --- /dev/null +++ b/codemods/pure-component/tests/export-default-function/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/export-default-function/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/mixed-superclasses/expected.tsx b/codemods/pure-component/tests/mixed-superclasses/expected.tsx new file mode 100644 index 0000000..f788443 --- /dev/null +++ b/codemods/pure-component/tests/mixed-superclasses/expected.tsx @@ -0,0 +1,7 @@ +import React, { PureComponent } from 'react'; + +function A(props) { return
    ; } + +class B extends PureComponent { + render() { return
    ; } +} diff --git a/codemods/pure-component/tests/mixed-superclasses/input.tsx b/codemods/pure-component/tests/mixed-superclasses/input.tsx new file mode 100644 index 0000000..dcf2b03 --- /dev/null +++ b/codemods/pure-component/tests/mixed-superclasses/input.tsx @@ -0,0 +1,9 @@ +import React, { Component, PureComponent } from 'react'; + +class A extends Component { + render() { return
    ; } +} + +class B extends PureComponent { + render() { return
    ; } +} diff --git a/codemods/pure-component/tests/mixed-superclasses/metrics.json b/codemods/pure-component/tests/mixed-superclasses/metrics.json new file mode 100644 index 0000000..b5fbb9c --- /dev/null +++ b/codemods/pure-component/tests/mixed-superclasses/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/mixed-superclasses/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/named-export-arrow/expected.tsx b/codemods/pure-component/tests/named-export-arrow/expected.tsx new file mode 100644 index 0000000..9254115 --- /dev/null +++ b/codemods/pure-component/tests/named-export-arrow/expected.tsx @@ -0,0 +1,3 @@ +import React from 'react'; + +export const Pure = props => { return
    ; }; diff --git a/codemods/pure-component/tests/named-export-arrow/input.tsx b/codemods/pure-component/tests/named-export-arrow/input.tsx new file mode 100644 index 0000000..8708ae0 --- /dev/null +++ b/codemods/pure-component/tests/named-export-arrow/input.tsx @@ -0,0 +1,5 @@ +import React, { Component } from 'react'; + +export class Pure extends Component { + render() { return
    ; } +} diff --git a/codemods/pure-component/tests/named-export-arrow/metrics.json b/codemods/pure-component/tests/named-export-arrow/metrics.json new file mode 100644 index 0000000..2a89fa5 --- /dev/null +++ b/codemods/pure-component/tests/named-export-arrow/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/named-export-arrow/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/named-export-arrow/test.config.json b/codemods/pure-component/tests/named-export-arrow/test.config.json new file mode 100644 index 0000000..8646dd5 --- /dev/null +++ b/codemods/pure-component/tests/named-export-arrow/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "useArrows": true + } +} diff --git a/codemods/pure-component/tests/named-export/expected.tsx b/codemods/pure-component/tests/named-export/expected.tsx new file mode 100644 index 0000000..e234c87 --- /dev/null +++ b/codemods/pure-component/tests/named-export/expected.tsx @@ -0,0 +1,3 @@ +import React from 'react'; + +export function Pure(props) { return
    ; } diff --git a/codemods/pure-component/tests/named-export/input.tsx b/codemods/pure-component/tests/named-export/input.tsx new file mode 100644 index 0000000..8708ae0 --- /dev/null +++ b/codemods/pure-component/tests/named-export/input.tsx @@ -0,0 +1,5 @@ +import React, { Component } from 'react'; + +export class Pure extends Component { + render() { return
    ; } +} diff --git a/codemods/pure-component/tests/named-export/metrics.json b/codemods/pure-component/tests/named-export/metrics.json new file mode 100644 index 0000000..6ca6bf0 --- /dev/null +++ b/codemods/pure-component/tests/named-export/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/named-export/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/named-import-component/expected.tsx b/codemods/pure-component/tests/named-import-component/expected.tsx new file mode 100644 index 0000000..904948f --- /dev/null +++ b/codemods/pure-component/tests/named-import-component/expected.tsx @@ -0,0 +1,3 @@ +import React from 'react'; + +function Pure(props) { return
    ; } diff --git a/codemods/pure-component/tests/named-import-component/input.tsx b/codemods/pure-component/tests/named-import-component/input.tsx new file mode 100644 index 0000000..2bd8659 --- /dev/null +++ b/codemods/pure-component/tests/named-import-component/input.tsx @@ -0,0 +1,5 @@ +import React, { Component } from 'react'; + +class Pure extends Component { + render() { return
    ; } +} diff --git a/codemods/pure-component/tests/named-import-component/metrics.json b/codemods/pure-component/tests/named-import-component/metrics.json new file mode 100644 index 0000000..d4d500b --- /dev/null +++ b/codemods/pure-component/tests/named-import-component/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/named-import-component/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/named-import-purecomponent-alias/expected.tsx b/codemods/pure-component/tests/named-import-purecomponent-alias/expected.tsx new file mode 100644 index 0000000..0cb2bd5 --- /dev/null +++ b/codemods/pure-component/tests/named-import-purecomponent-alias/expected.tsx @@ -0,0 +1,3 @@ +import React, { PureComponent as P } from 'react'; + +function Pure(props) { return
    ; } diff --git a/codemods/pure-component/tests/named-import-purecomponent-alias/input.tsx b/codemods/pure-component/tests/named-import-purecomponent-alias/input.tsx new file mode 100644 index 0000000..270c814 --- /dev/null +++ b/codemods/pure-component/tests/named-import-purecomponent-alias/input.tsx @@ -0,0 +1,5 @@ +import React, { PureComponent as P } from 'react'; + +class Pure extends P { + render() { return
    ; } +} diff --git a/codemods/pure-component/tests/named-import-purecomponent-alias/metrics.json b/codemods/pure-component/tests/named-import-purecomponent-alias/metrics.json new file mode 100644 index 0000000..efaa063 --- /dev/null +++ b/codemods/pure-component/tests/named-import-purecomponent-alias/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/named-import-purecomponent-alias/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-component/tests/react-native-named-import/expected.tsx b/codemods/pure-component/tests/react-native-named-import/expected.tsx new file mode 100644 index 0000000..6901793 --- /dev/null +++ b/codemods/pure-component/tests/react-native-named-import/expected.tsx @@ -0,0 +1,3 @@ +import 'react-native'; + +function Pure(props) { return
    ; } diff --git a/codemods/pure-component/tests/react-native-named-import/input.tsx b/codemods/pure-component/tests/react-native-named-import/input.tsx new file mode 100644 index 0000000..56e7352 --- /dev/null +++ b/codemods/pure-component/tests/react-native-named-import/input.tsx @@ -0,0 +1,5 @@ +import { Component } from 'react-native'; + +class Pure extends Component { + render() { return
    ; } +} diff --git a/codemods/pure-component/tests/react-native-named-import/metrics.json b/codemods/pure-component/tests/react-native-named-import/metrics.json new file mode 100644 index 0000000..419d2d6 --- /dev/null +++ b/codemods/pure-component/tests/react-native-named-import/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-component-conversions": [ + { + "cardinality": { + "file": "tests/react-native-named-import/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file From ad5bf85508fa775f1d6f95258006e4dbc25fb93f Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Tue, 14 Apr 2026 22:31:19 +0200 Subject: [PATCH 08/29] fix create-element-to-jsx --- .../create-element-to-jsx/scripts/codemod.ts | 50 ++++++--------- .../scripts/differential-tests.mjs | 64 +++++-------------- 2 files changed, 35 insertions(+), 79 deletions(-) diff --git a/codemods/create-element-to-jsx/scripts/codemod.ts b/codemods/create-element-to-jsx/scripts/codemod.ts index 791c443..458ea1c 100644 --- a/codemods/create-element-to-jsx/scripts/codemod.ts +++ b/codemods/create-element-to-jsx/scripts/codemod.ts @@ -10,7 +10,7 @@ type Conversion = { type TagDescriptor = { text: string; - trailingComments: string[]; + identifierComments: string[]; }; type AttrDescriptor = { @@ -25,7 +25,7 @@ type RenderedElement = { leadingComments: string[]; trailingComments: string[]; hasComments: boolean; - wrappedChildTrailingComments?: string[]; + expressionTrailingComments?: string[]; }; type ArgumentLayout = { @@ -380,11 +380,11 @@ function splitBoundaryCommentsByComma( function tagDescriptor(node: SgNode): TagDescriptor | null { if (node.kind() === "identifier" || node.kind() === "property_identifier") { - return { text: node.text(), trailingComments: directCommentTexts(node) }; + return { text: node.text(), identifierComments: directCommentTexts(node) }; } if (node.kind() === "string") { - return { text: stringValue(node), trailingComments: directCommentTexts(node) }; + return { text: stringValue(node), identifierComments: directCommentTexts(node) }; } if (node.kind() !== "member_expression") { @@ -403,18 +403,13 @@ function tagDescriptor(node: SgNode): TagDescriptor | null { return null; } - const subtreeComments = node.findAll({ rule: { kind: "comment" } }) - .sort((left, right) => left.range().start.index - right.range().start.index) - .map((comment) => comment.text()); - const trailingComments = [ - ...directCommentTexts(node), - ...objectTag.trailingComments, - ...propertyTag.trailingComments, - ]; - return { text: `${objectTag.text}.${propertyTag.text}`, - trailingComments, + identifierComments: [ + ...directCommentTexts(node), + ...objectTag.identifierComments, + ...propertyTag.identifierComments, + ], }; } @@ -1062,7 +1057,7 @@ function commentedChild( node.range().end.index, source, ), - ...(nested.wrappedChildTrailingComments ?? nested.trailingComments), + ...(nested.expressionTrailingComments ?? nested.trailingComments), ], baseIndent, ), @@ -1120,20 +1115,14 @@ function convertCommentedCall(call: SgNode, baseIndent: string, source: str const baseTagPropsTrailing = (propsIsSpreadLike || propsIsNull) ? [] : betweenTagAndProps.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()); - const memberNoPropsBoundaryComments = !propsArg && elementArg.kind() === "member_expression" - ? [ - ...beforeFirst.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()), - ...baseTagPropsTrailing, - ] - : []; const trailingComments = !propsArg && elementArg.kind() === "member_expression" ? [ ...beforeFirst.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()), ...baseTagPropsTrailing, - ...tagInfo.trailingComments, + ...tagInfo.identifierComments, ] : [ - ...tagInfo.trailingComments, + ...tagInfo.identifierComments, ...beforeFirst.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()), ...baseTagPropsTrailing, ]; @@ -1195,14 +1184,15 @@ function convertCommentedCall(call: SgNode, baseIndent: string, source: str trailingComments.length > 0 || props.attrs.some((attr) => attr.leadingBlocks.length > 0) || children.some((child) => child.code.includes("\n")), - wrappedChildTrailingComments: - !propsArg && elementArg.kind() === "member_expression" && memberNoPropsBoundaryComments.length > 0 + // Legacy recast printing duplicates a wrapped member-expression element's + // element-level trailing comments, then prints identifier-local comments once + // more. Keep those buckets separate and derive the wrapped form from them. + expressionTrailingComments: + !propsArg && elementArg.kind() === "member_expression" ? [ - ...memberNoPropsBoundaryComments, - ...tagInfo.trailingComments, - ...memberNoPropsBoundaryComments, - ...tagInfo.trailingComments, - ...tagInfo.trailingComments, + ...trailingComments, + ...trailingComments, + ...tagInfo.identifierComments, ] : undefined, }; diff --git a/codemods/create-element-to-jsx/scripts/differential-tests.mjs b/codemods/create-element-to-jsx/scripts/differential-tests.mjs index 1a855ab..bfd1d5b 100644 --- a/codemods/create-element-to-jsx/scripts/differential-tests.mjs +++ b/codemods/create-element-to-jsx/scripts/differential-tests.mjs @@ -9,37 +9,10 @@ import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); const packageDir = path.dirname(path.dirname(__filename)); const workflowPath = path.join(packageDir, "workflow.yaml"); -const legacyTransformPath = "/Users/mohabsameh/Downloads/react-codemod/transforms/create-element-to-jsx.js"; +const testsDir = path.join(packageDir, "tests"); -function runLegacy(source) { - const dir = mkdtempSync(path.join(tmpdir(), "ce2jsx-legacy-")); - const inputPath = path.join(dir, "input.js"); - writeFileSync(inputPath, source, "utf8"); - - try { - const result = spawnSync( - "npx", - [ - "--yes", - "jscodeshift", - "-t", - legacyTransformPath, - inputPath, - "--run-in-band", - "--parser", - "flow", - ], - { - cwd: packageDir, - encoding: "utf8", - timeout: 20000, - }, - ); - assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); - return readFileSync(inputPath, "utf8").trim(); - } finally { - rmSync(dir, { recursive: true, force: true }); - } +function readFixture(name, file) { + return readFileSync(path.join(testsDir, name, file), "utf8").trim(); } function runJssg(source) { @@ -79,25 +52,18 @@ function runJssg(source) { } } -const parityCases = { - "member_simple_comments": `var React = require('react'); -React.createElement(/*A*/Foo/*B*/./*C*/Bar/*D*/); -`, - "spread_prop_comments": `var React = require('react'); -React.createElement('div', /*A*/getProps()/*B*/, 'x'); -`, - "member_child_comments_alt": `var React = require('react'); -React.createElement('div', null, React.createElement(/*A*/Foo/*B*/./*C*/Bar/*D*/) /*E*/); -`, - "comment_only_props_string_child": `var React = require('react'); -React.createElement(Foo, {/*P*/}, 'x'); -`, -}; +const parityCases = [ + "member-simple-comments", + "spread-prop-comments", + "member-child-comments-alt", + "comment-only-props-string-child", +]; -for (const [name, source] of Object.entries(parityCases)) { - test(`matches legacy output for ${name}`, () => { - const legacyOutput = runLegacy(source); - const jssgOutput = runJssg(source); - assert.strictEqual(jssgOutput, legacyOutput); +for (const name of parityCases) { + test(`matches checked-in parity fixture for ${name}`, () => { + const input = readFixture(name, "input.tsx"); + const expected = readFixture(name, "expected.tsx"); + const jssgOutput = runJssg(input); + assert.strictEqual(jssgOutput, expected); }); } From f9831c7215e57180a89d9d9c59f9373527c4d048 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Tue, 14 Apr 2026 23:07:39 +0200 Subject: [PATCH 09/29] fix codemods --- codemods/find-dom-node/README.md | 4 + codemods/find-dom-node/scripts/codemod.ts | 184 ++++++++++++++--- .../tests/es6-class-no-change/expected.tsx | 7 + .../tests/es6-class-no-change/input.tsx | 7 + .../tests/export-default/expected.tsx | 7 + .../tests/export-default/input.tsx | 7 + .../tests/export-default/metrics.json | 10 + .../tests/helper-no-change/expected.tsx | 2 + .../tests/helper-no-change/input.tsx | 2 + .../tests/module-exports/expected.tsx | 7 + .../tests/module-exports/input.tsx | 7 + .../tests/module-exports/metrics.json | 10 + .../expected.tsx | 5 + .../input.tsx | 5 + .../metrics.json | 10 + .../test.config.json | 5 + .../tests/no-react-create-class/expected.tsx | 5 + .../tests/no-react-create-class/input.tsx | 5 + codemods/pure-render-mixin/README.md | 1 + codemods/pure-render-mixin/scripts/codemod.ts | 90 +++++++-- .../expected.tsx | 11 + .../input.tsx | 11 + .../no-react-explicit-require/expected.tsx | 12 ++ .../tests/no-react-explicit-require/input.tsx | 9 + .../no-react-explicit-require/metrics.json | 10 + .../test.config.json | 5 + .../tests/no-react-no-change/expected.tsx | 9 + .../tests/no-react-no-change/input.tsx | 9 + codemods/sort-comp/README.md | 1 + codemods/sort-comp/scripts/codemod.ts | 188 +++++++++++++++--- .../expected.tsx | 9 + .../input.tsx | 9 + .../no-react-explicit-require/expected.tsx | 7 + .../tests/no-react-explicit-require/input.tsx | 7 + .../no-react-explicit-require/metrics.json | 11 + .../test.config.json | 5 + .../tests/no-react-no-change/expected.tsx | 7 + .../tests/no-react-no-change/input.tsx | 7 + .../expected.tsx | 9 + .../unrelated-component-no-change/input.tsx | 9 + pnpm-lock.yaml | 138 +++++++++++-- 41 files changed, 763 insertions(+), 100 deletions(-) create mode 100644 codemods/find-dom-node/tests/es6-class-no-change/expected.tsx create mode 100644 codemods/find-dom-node/tests/es6-class-no-change/input.tsx create mode 100644 codemods/find-dom-node/tests/export-default/expected.tsx create mode 100644 codemods/find-dom-node/tests/export-default/input.tsx create mode 100644 codemods/find-dom-node/tests/export-default/metrics.json create mode 100644 codemods/find-dom-node/tests/helper-no-change/expected.tsx create mode 100644 codemods/find-dom-node/tests/helper-no-change/input.tsx create mode 100644 codemods/find-dom-node/tests/module-exports/expected.tsx create mode 100644 codemods/find-dom-node/tests/module-exports/input.tsx create mode 100644 codemods/find-dom-node/tests/module-exports/metrics.json create mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx create mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx create mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json create mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json create mode 100644 codemods/find-dom-node/tests/no-react-create-class/expected.tsx create mode 100644 codemods/find-dom-node/tests/no-react-create-class/input.tsx create mode 100644 codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx create mode 100644 codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx create mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx create mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx create mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json create mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json create mode 100644 codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx create mode 100644 codemods/pure-render-mixin/tests/no-react-no-change/input.tsx create mode 100644 codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx create mode 100644 codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx create mode 100644 codemods/sort-comp/tests/no-react-explicit-require/expected.tsx create mode 100644 codemods/sort-comp/tests/no-react-explicit-require/input.tsx create mode 100644 codemods/sort-comp/tests/no-react-explicit-require/metrics.json create mode 100644 codemods/sort-comp/tests/no-react-explicit-require/test.config.json create mode 100644 codemods/sort-comp/tests/no-react-no-change/expected.tsx create mode 100644 codemods/sort-comp/tests/no-react-no-change/input.tsx create mode 100644 codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx create mode 100644 codemods/sort-comp/tests/unrelated-component-no-change/input.tsx diff --git a/codemods/find-dom-node/README.md b/codemods/find-dom-node/README.md index 222ed0c..2ffc54d 100644 --- a/codemods/find-dom-node/README.md +++ b/codemods/find-dom-node/README.md @@ -8,6 +8,10 @@ Replace `.getDOMNode()` calls with `React.findDOMNode(...)`. codemod run @react/find-dom-node ``` +## Options + +- `explicit-require`: when `false`, run even if no React import/require is present. Default: `true`. + ## Development ```bash diff --git a/codemods/find-dom-node/scripts/codemod.ts b/codemods/find-dom-node/scripts/codemod.ts index b5584df..66e8360 100644 --- a/codemods/find-dom-node/scripts/codemod.ts +++ b/codemods/find-dom-node/scripts/codemod.ts @@ -1,53 +1,179 @@ -import type { Transform, Edit } from "codemod:ast-grep"; +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; + +const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); function metricFile(filename: string): string { const cwd = process.cwd() + "/"; return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; } +function stringValue(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = node.text(); + return text.length >= 2 ? text.slice(1, -1) : null; +} + +function callArguments(call: SgNode): SgNode[] { + const args = call.field("arguments"); + if (!args) return []; + return args.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function requireSource(call: SgNode): string | null { + const callee = call.field("function"); + if (!callee || callee.kind() !== "identifier" || callee.text() !== "require") return null; + const firstArg = callArguments(call)[0]; + return firstArg?.kind() === "string" ? stringValue(firstArg) : null; +} + +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? stringValue(source) : null; +} + +function hasReact(rootNode: SgNode): boolean { + for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (REACT_MODULES.has(importSource(importNode) ?? "")) return true; + } + + for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { + if (REACT_MODULES.has(requireSource(call) ?? "")) return true; + } + + return false; +} + +function explicitRequireDisabled(value: unknown): boolean { + return value === false || value === "false"; +} -const transform: Transform = async (root) => { +function isReactCreateClassCall(node: SgNode | null): node is SgNode { + if (!node || node.kind() !== "call_expression") return false; + const callee = node.field("function"); + return callee?.kind() === "member_expression" && + callee.field("object")?.kind() === "identifier" && + callee.field("object")?.text() === "React" && + callee.field("property")?.kind() === "property_identifier" && + callee.field("property")?.text() === "createClass"; +} + +function isModuleExportsMember(node: SgNode | null): boolean { + return node?.kind() === "member_expression" && + node.field("object")?.kind() === "identifier" && + node.field("object")?.text() === "module" && + node.field("property")?.kind() === "property_identifier" && + node.field("property")?.text() === "exports"; +} + +function createClassContainers(rootNode: SgNode): SgNode[] { + const containers: SgNode[] = []; + + for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { + if (isReactCreateClassCall(declarator.field("value"))) containers.push(declarator); + } + + for (const assignment of rootNode.findAll({ rule: { kind: "assignment_expression" } })) { + if (isModuleExportsMember(assignment.field("left")) && isReactCreateClassCall(assignment.field("right"))) { + containers.push(assignment); + } + } + + for (const exportStmt of rootNode.findAll({ rule: { kind: "export_statement" } })) { + const exported = exportStmt.children().find((child) => child.isNamed() && child.kind() === "call_expression"); + if (isReactCreateClassCall(exported ?? null)) containers.push(exportStmt); + } + + return containers; +} + +function isThisRefsBase(node: SgNode | null): boolean { + return node?.kind() === "member_expression" && + node.field("object")?.kind() === "this" && + node.field("property")?.kind() === "property_identifier" && + node.field("property")?.text() === "refs"; +} + +function accessObject(node: SgNode | null): SgNode | null { + if (!node) return null; + if (node.kind() === "member_expression" || node.kind() === "subscript_expression") { + return node.field("object"); + } + return null; +} + +function isThisRefsChain(node: SgNode | null): boolean { + if (!node) return false; + const object = accessObject(node); + if (!object) return false; + if (isThisRefsBase(object)) return true; + return isThisRefsChain(object); +} + +function isDirectRefsAlias(node: SgNode | null): boolean { + if (!node) return false; + const object = accessObject(node); + return isThisRefsBase(object); +} + +function isGetDOMNodeCall(call: SgNode, objectMatcher: (node: SgNode | null) => boolean): boolean { + const callee = call.field("function"); + return callee?.kind() === "member_expression" && + callee.field("property")?.kind() === "property_identifier" && + callee.field("property")?.text() === "getDOMNode" && + objectMatcher(callee.field("object")); +} + +const transform: Transform = async (root, options) => { const rootNode = root.root(); - const edits: Edit[] = []; + if (!explicitRequireDisabled(options.params?.["explicit-require"]) && !hasReact(rootNode)) { + return null; + } - const findDOMNodeMetric = useMetricAtom("find-dom-node-replacements"); + const metric = useMetricAtom("find-dom-node-replacements"); + const edits: Edit[] = []; + let replacements = 0; - const reactDefaultImport = getImport(rootNode, { type: "default", from: "react" }); - const reactName = reactDefaultImport?.alias ?? "React"; + for (const container of createClassContainers(rootNode)) { + for (const call of container.findAll({ rule: { kind: "call_expression" } })) { + if (isGetDOMNodeCall(call, (node) => node?.kind() === "this")) { + edits.push(call.replace("React.findDOMNode(this)")); + replacements++; + continue; + } - const getDOMNodeCalls = rootNode.findAll({ - rule: { - kind: "call_expression", - has: { - field: "function", - kind: "member_expression", - all: [ - { has: { field: "property", kind: "property_identifier", regex: "^getDOMNode$" } }, - ], - }, - }, - }); + if (isGetDOMNodeCall(call, isThisRefsChain)) { + const target = call.field("function")?.field("object"); + if (!target) continue; + edits.push(call.replace(`React.findDOMNode(${target.text()})`)); + replacements++; + } + } - for (const call of getDOMNodeCalls) { - const callee = call.field("function"); - if (!callee) continue; + for (const declarator of container.findAll({ rule: { kind: "variable_declarator" } })) { + const name = declarator.field("name"); + const value = declarator.field("value"); + if (name?.kind() !== "identifier" || !isDirectRefsAlias(value)) continue; - const objectNode = callee.field("object"); - if (!objectNode) continue; + const functionExpression = declarator.ancestors().find((ancestor) => ancestor.kind() === "function_expression"); + if (!functionExpression) continue; - const replacement = `${reactName}.findDOMNode(${objectNode.text()})`; - edits.push(call.replace(replacement)); + for (const call of functionExpression.findAll({ rule: { kind: "call_expression" } })) { + if (!isGetDOMNodeCall(call, (node) => node?.kind() === "identifier" && node.text() === name.text())) { + continue; + } - findDOMNodeMetric.increment({ - file: metricFile(root.filename()), - }); + edits.push(call.replace(`React.findDOMNode(${name.text()})`)); + replacements++; + } + } } if (edits.length === 0) return null; + metric.increment({ file: metricFile(root.filename()) }, replacements); return rootNode.commitEdits(edits); }; diff --git a/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx b/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx new file mode 100644 index 0000000..464aee7 --- /dev/null +++ b/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +class C extends React.Component { + render() { + return this.getDOMNode(); + } +} diff --git a/codemods/find-dom-node/tests/es6-class-no-change/input.tsx b/codemods/find-dom-node/tests/es6-class-no-change/input.tsx new file mode 100644 index 0000000..464aee7 --- /dev/null +++ b/codemods/find-dom-node/tests/es6-class-no-change/input.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +class C extends React.Component { + render() { + return this.getDOMNode(); + } +} diff --git a/codemods/find-dom-node/tests/export-default/expected.tsx b/codemods/find-dom-node/tests/export-default/expected.tsx new file mode 100644 index 0000000..ae81e09 --- /dev/null +++ b/codemods/find-dom-node/tests/export-default/expected.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +export default React.createClass({ + render: function() { + return React.findDOMNode(this); + }, +}); diff --git a/codemods/find-dom-node/tests/export-default/input.tsx b/codemods/find-dom-node/tests/export-default/input.tsx new file mode 100644 index 0000000..97fa6eb --- /dev/null +++ b/codemods/find-dom-node/tests/export-default/input.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +export default React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/find-dom-node/tests/export-default/metrics.json b/codemods/find-dom-node/tests/export-default/metrics.json new file mode 100644 index 0000000..6d1bd65 --- /dev/null +++ b/codemods/find-dom-node/tests/export-default/metrics.json @@ -0,0 +1,10 @@ +{ + "find-dom-node-replacements": [ + { + "cardinality": { + "file": "tests/export-default/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/helper-no-change/expected.tsx b/codemods/find-dom-node/tests/helper-no-change/expected.tsx new file mode 100644 index 0000000..281d7fa --- /dev/null +++ b/codemods/find-dom-node/tests/helper-no-change/expected.tsx @@ -0,0 +1,2 @@ +const helper = foo(); +helper.getDOMNode(); diff --git a/codemods/find-dom-node/tests/helper-no-change/input.tsx b/codemods/find-dom-node/tests/helper-no-change/input.tsx new file mode 100644 index 0000000..281d7fa --- /dev/null +++ b/codemods/find-dom-node/tests/helper-no-change/input.tsx @@ -0,0 +1,2 @@ +const helper = foo(); +helper.getDOMNode(); diff --git a/codemods/find-dom-node/tests/module-exports/expected.tsx b/codemods/find-dom-node/tests/module-exports/expected.tsx new file mode 100644 index 0000000..cab9f4c --- /dev/null +++ b/codemods/find-dom-node/tests/module-exports/expected.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +module.exports = React.createClass({ + render: function() { + return React.findDOMNode(this); + }, +}); diff --git a/codemods/find-dom-node/tests/module-exports/input.tsx b/codemods/find-dom-node/tests/module-exports/input.tsx new file mode 100644 index 0000000..9658cf5 --- /dev/null +++ b/codemods/find-dom-node/tests/module-exports/input.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +module.exports = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/find-dom-node/tests/module-exports/metrics.json b/codemods/find-dom-node/tests/module-exports/metrics.json new file mode 100644 index 0000000..42a4df1 --- /dev/null +++ b/codemods/find-dom-node/tests/module-exports/metrics.json @@ -0,0 +1,10 @@ +{ + "find-dom-node-replacements": [ + { + "cardinality": { + "file": "tests/module-exports/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx new file mode 100644 index 0000000..dd2e29f --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx @@ -0,0 +1,5 @@ +var Composer = React.createClass({ + render: function() { + return React.findDOMNode(this); + }, +}); diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx new file mode 100644 index 0000000..8b8bbf5 --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx @@ -0,0 +1,5 @@ +var Composer = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json new file mode 100644 index 0000000..6b828be --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json @@ -0,0 +1,10 @@ +{ + "find-dom-node-replacements": [ + { + "cardinality": { + "file": "tests/no-react-create-class-explicit-require/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json new file mode 100644 index 0000000..4858435 --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "explicit-require": false + } +} diff --git a/codemods/find-dom-node/tests/no-react-create-class/expected.tsx b/codemods/find-dom-node/tests/no-react-create-class/expected.tsx new file mode 100644 index 0000000..8b8bbf5 --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class/expected.tsx @@ -0,0 +1,5 @@ +var Composer = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/find-dom-node/tests/no-react-create-class/input.tsx b/codemods/find-dom-node/tests/no-react-create-class/input.tsx new file mode 100644 index 0000000..8b8bbf5 --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class/input.tsx @@ -0,0 +1,5 @@ +var Composer = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/pure-render-mixin/README.md b/codemods/pure-render-mixin/README.md index be9571a..847c05c 100644 --- a/codemods/pure-render-mixin/README.md +++ b/codemods/pure-render-mixin/README.md @@ -11,6 +11,7 @@ codemod run @react/pure-render-mixin ## Options - `mixin-name`: custom mixin identifier to replace. Default: `PureRenderMixin`. +- `explicit-require`: when `false`, run even if no React import/require is present. Default: `true`. ## Development diff --git a/codemods/pure-render-mixin/scripts/codemod.ts b/codemods/pure-render-mixin/scripts/codemod.ts index 0598203..8107706 100644 --- a/codemods/pure-render-mixin/scripts/codemod.ts +++ b/codemods/pure-render-mixin/scripts/codemod.ts @@ -2,11 +2,76 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); + function metricFile(filename: string): string { const cwd = process.cwd() + "/"; return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; } +function stringValue(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = node.text(); + return text.length >= 2 ? text.slice(1, -1) : null; +} + +function callArguments(call: SgNode): SgNode[] { + const args = call.field("arguments"); + if (!args) return []; + return args.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function requireSource(call: SgNode): string | null { + const callee = call.field("function"); + if (!callee || callee.kind() !== "identifier" || callee.text() !== "require") return null; + const firstArg = callArguments(call)[0]; + return firstArg?.kind() === "string" ? stringValue(firstArg) : null; +} + +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? stringValue(source) : null; +} + +function hasReact(rootNode: SgNode): boolean { + for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (REACT_MODULES.has(importSource(importNode) ?? "")) return true; + } + + for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { + if (REACT_MODULES.has(requireSource(call) ?? "")) return true; + } + + return false; +} + +function explicitRequireDisabled(value: unknown): boolean { + return value === false || value === "false"; +} + +function isReactCreateClassCall(node: SgNode | null): node is SgNode { + if (!node || node.kind() !== "call_expression") return false; + const callee = node.field("function"); + return callee?.kind() === "member_expression" && + callee.field("object")?.kind() === "identifier" && + callee.field("object")?.text() === "React" && + callee.field("property")?.kind() === "property_identifier" && + callee.field("property")?.text() === "createClass"; +} + +function reactCreateClassDeclarators(rootNode: SgNode): Array<{ + declarator: SgNode; + call: SgNode; +}> { + const result: Array<{ declarator: SgNode; call: SgNode }> = []; + for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { + const value = declarator.field("value"); + if (isReactCreateClassCall(value)) result.push({ declarator, call: value }); + } + return result; +} + const DEFAULT_MIXIN_NAME = "PureRenderMixin"; const SHOULD_COMPONENT_UPDATE = "shouldComponentUpdate"; @@ -45,6 +110,10 @@ function getPairKeyName(node: SgNode): string { const transform: Transform = async (root, options) => { const rootNode = root.root(); + if (!explicitRequireDisabled(options.params?.["explicit-require"]) && !hasReact(rootNode)) { + return null; + } + const source = rootNode.text(); const edits: Edit[] = []; @@ -52,26 +121,7 @@ const transform: Transform = async (root, options) => { const metric = useMetricAtom("pure-render-mixin-replacements"); let transformCount = 0; - const createClassCalls = rootNode.findAll({ - rule: { - kind: "call_expression", - has: { - field: "function", - any: [ - { - kind: "member_expression", - all: [ - { has: { field: "object", kind: "identifier", regex: "^React$" } }, - { has: { field: "property", kind: "property_identifier", regex: "^createClass$" } }, - ], - }, - { kind: "identifier", regex: "^createClass$" }, - ], - }, - }, - }); - - for (const call of createClassCalls) { + for (const { call } of reactCreateClassDeclarators(rootNode)) { const configObj = getConfigObject(call); if (!configObj) continue; diff --git a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx new file mode 100644 index 0000000..bd81dcc --- /dev/null +++ b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx @@ -0,0 +1,11 @@ +var React = require('react/addons'); +var PureRenderMixin = React.addons.PureRenderMixin; +var createClass = require('create-react-class'); + +var C = createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx new file mode 100644 index 0000000..bd81dcc --- /dev/null +++ b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx @@ -0,0 +1,11 @@ +var React = require('react/addons'); +var PureRenderMixin = React.addons.PureRenderMixin; +var createClass = require('create-react-class'); + +var C = createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx b/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx new file mode 100644 index 0000000..c0d6447 --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx @@ -0,0 +1,12 @@ +var C = React.createClass({ + + shouldComponentUpdate: function(nextProps, nextState) { + + return React.addons.shallowCompare(this, nextProps, nextState); + + }, + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx b/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx new file mode 100644 index 0000000..13cd748 --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx @@ -0,0 +1,9 @@ +var PureRenderMixin = React.addons.PureRenderMixin; + +var C = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json b/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json new file mode 100644 index 0000000..fd0f9ae --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-render-mixin-replacements": [ + { + "cardinality": { + "file": "tests/no-react-explicit-require/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json b/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json new file mode 100644 index 0000000..4858435 --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "explicit-require": false + } +} diff --git a/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx b/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx new file mode 100644 index 0000000..13cd748 --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx @@ -0,0 +1,9 @@ +var PureRenderMixin = React.addons.PureRenderMixin; + +var C = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx b/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx new file mode 100644 index 0000000..13cd748 --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx @@ -0,0 +1,9 @@ +var PureRenderMixin = React.addons.PureRenderMixin; + +var C = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/sort-comp/README.md b/codemods/sort-comp/README.md index 1920cad..0457d52 100644 --- a/codemods/sort-comp/README.md +++ b/codemods/sort-comp/README.md @@ -11,6 +11,7 @@ codemod run @react/sort-comp ## Options - `methodsOrder`: ordered list of method groups and names used for sorting. Defaults to the built-in React sort order used by this package. +- `explicit-require`: when `false`, run even if no React import/require is present. Default: `true`. ## Development diff --git a/codemods/sort-comp/scripts/codemod.ts b/codemods/sort-comp/scripts/codemod.ts index 44abfd3..5b7a501 100644 --- a/codemods/sort-comp/scripts/codemod.ts +++ b/codemods/sort-comp/scripts/codemod.ts @@ -7,6 +7,145 @@ function metricFile(filename: string): string { return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; } +type ReactImportAliases = { + component: Set; + pureComponent: Set; +}; + +type ReactSuperclassKind = "Component" | "PureComponent"; + +const REACT_SOURCES = new Set(["react", "React", "react/addons", "react-native"]); + +function stringValue(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = node.text(); + return text.length >= 2 ? text.slice(1, -1) : null; +} + +function callArguments(call: SgNode): SgNode[] { + const args = call.field("arguments"); + if (!args) return []; + return args.children().filter((child) => child.isNamed() && child.kind() !== "comment"); +} + +function requireSource(call: SgNode): string | null { + const callee = call.field("function"); + if (!callee || callee.kind() !== "identifier" || callee.text() !== "require") return null; + const firstArg = callArguments(call)[0]; + return firstArg?.kind() === "string" ? stringValue(firstArg) : null; +} + +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? stringValue(source) : null; +} + +function isReactSource(importStmt: SgNode): boolean { + return REACT_SOURCES.has(importSource(importStmt) ?? ""); +} + +function hasReact(rootNode: SgNode): boolean { + for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (isReactSource(importNode)) return true; + } + + for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { + if (REACT_SOURCES.has(requireSource(call) ?? "")) return true; + } + + return false; +} + +function explicitRequireDisabled(value: unknown): boolean { + return value === false || value === "false"; +} + +function reactImportAliases(rootNode: SgNode): ReactImportAliases { + const component = new Set(); + const pureComponent = new Set(); + + for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (!isReactSource(importStmt)) continue; + + for (const specifier of importStmt.findAll({ rule: { kind: "import_specifier" } })) { + const imported = specifier.field("name")?.text(); + const alias = specifier.field("alias")?.text() ?? imported; + if (!imported || !alias) continue; + if (imported === "Component") component.add(alias); + if (imported === "PureComponent") pureComponent.add(alias); + } + } + + return { component, pureComponent }; +} + +function reactSuperclassKind( + classDecl: SgNode, + aliases: ReactImportAliases, +): ReactSuperclassKind | null { + const heritage = classDecl.find({ rule: { kind: "class_heritage" } }); + if (!heritage) return null; + + const directExtends = heritage.find({ + rule: { any: [{ kind: "member_expression" }, { kind: "identifier" }] }, + }); + if (!directExtends) return null; + + if (directExtends.kind() === "member_expression") { + const object = directExtends.field("object"); + const property = directExtends.field("property")?.text() ?? ""; + if (object?.text() !== "React") return null; + if (property === "Component" || property === "PureComponent") return property; + return null; + } + + const name = directExtends.text(); + if (aliases.component.has(name)) return "Component"; + if (aliases.pureComponent.has(name)) return "PureComponent"; + return null; +} + +function isReactCreateClassCall(node: SgNode | null): node is SgNode { + if (!node || node.kind() !== "call_expression") return false; + const callee = node.field("function"); + return callee?.kind() === "member_expression" && + callee.field("object")?.kind() === "identifier" && + callee.field("object")?.text() === "React" && + callee.field("property")?.kind() === "property_identifier" && + callee.field("property")?.text() === "createClass"; +} + +function reactCreateClassConfigs(rootNode: SgNode): SgNode[] { + const configs: SgNode[] = []; + for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { + const value = declarator.field("value"); + if (!isReactCreateClassCall(value)) continue; + const args = value.field("arguments"); + const config = args?.children().find((child) => child.kind() === "object"); + if (config) configs.push(config); + } + return configs; +} + +function reactClassBodies(rootNode: SgNode): SgNode[] { + const aliases = reactImportAliases(rootNode); + const reactClasses = rootNode.findAll({ rule: { kind: "class_declaration" } }) + .map((classDecl) => ({ classDecl, kind: reactSuperclassKind(classDecl, aliases) })) + .filter((entry): entry is { classDecl: SgNode; kind: ReactSuperclassKind } => entry.kind !== null); + const targetSuperclassKind = reactClasses.some((entry) => entry.kind === "Component") + ? "Component" + : reactClasses.some((entry) => entry.kind === "PureComponent") + ? "PureComponent" + : null; + + if (!targetSuperclassKind) return []; + return reactClasses + .filter((entry) => entry.kind === targetSuperclassKind) + .map((entry) => entry.classDecl.find({ rule: { kind: "class_body" } })) + .filter((body): body is SgNode => body !== null); +} + type MemberChunk = { node: SgNode; @@ -51,8 +190,15 @@ const DEFAULT_METHODS_ORDER = [ "render", ]; +function isTypeAnnotationMember(node: SgNode): boolean { + return node.kind() === "public_field_definition" && + node.field("value") === null && + node.find({ rule: { kind: "type_annotation" } }) !== null; +} + function selectorMatches(selector: string, name: string, isStatic: boolean): boolean { if (selector === "static-methods") return isStatic; + if (selector === "type-annotations") return false; if (selector === name) return true; if (selector.startsWith("/") && selector.lastIndexOf("/") > 0) { const lastSlash = selector.lastIndexOf("/"); @@ -67,11 +213,15 @@ function selectorMatches(selector: string, name: string, isStatic: boolean): boo return false; } +function chunkMatchesSelector(selector: string, chunk: MemberChunk): boolean { + if (selector === "type-annotations") return isTypeAnnotationMember(chunk.node); + return selectorMatches(selector, chunk.name, chunk.node.text().startsWith("static ")); +} + function correctIndex(order: string[], chunk: MemberChunk): number { const everythingElse = order.indexOf("everything-else"); - const isStatic = chunk.node.text().startsWith("static "); for (let i = 0; i < order.length; i++) { - if (i !== everythingElse && selectorMatches(order[i]!, chunk.name, isStatic)) return i; + if (i !== everythingElse && chunkMatchesSelector(order[i]!, chunk)) return i; } return everythingElse >= 0 ? everythingElse : Number.POSITIVE_INFINITY; } @@ -129,26 +279,6 @@ function sortChunks(chunks: MemberChunk[], order: string[]): MemberChunk[] { }); } -function createClassConfig(call: SgNode): SgNode | null { - const args = call.field("arguments"); - if (!args) return null; - return args.children().find((child) => child.kind() === "object") ?? null; -} - -function isCreateClassCall(call: SgNode): boolean { - const fn = call.field("function"); - if (!fn) return false; - if (fn.kind() === "identifier") return fn.text() === "createClass"; - return fn.kind() === "member_expression" && fn.field("property")?.text() === "createClass"; -} - -function isReactClass(node: SgNode): boolean { - const heritage = node.find({ rule: { kind: "class_heritage" } }); - if (!heritage) return false; - return heritage.findAll({ rule: { kind: "identifier" } }) - .some((id) => id.text() === "Component" || id.text() === "PureComponent" || id.text() === "React"); -} - function replacementFor(container: SgNode, chunks: MemberChunk[], order: string[], sep: string): Edit | null { if (chunks.length <= 1) return null; const sorted = sortChunks(chunks, order); @@ -163,6 +293,10 @@ function replacementFor(container: SgNode, chunks: MemberChunk[], order: st const transform: Transform = async (root, options) => { const rootNode = root.root(); + if (!explicitRequireDisabled(options.params?.["explicit-require"]) && !hasReact(rootNode)) { + return null; + } + const source = rootNode.text(); const edits: Edit[] = []; const metric = useMetricAtom("sort-comp-reorders"); @@ -171,10 +305,7 @@ const transform: Transform = async (root, options) => { ? methodsOrderParam.filter((item): item is string => typeof item === "string") : DEFAULT_METHODS_ORDER; - for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - if (!isCreateClassCall(call)) continue; - const config = createClassConfig(call); - if (!config) continue; + for (const config of reactCreateClassConfigs(rootNode)) { const edit = replacementFor(config, collectChunks(config, source), methodsOrder, ",\n\n "); if (edit) { edits.push(edit); @@ -182,10 +313,7 @@ const transform: Transform = async (root, options) => { } } - for (const classDecl of rootNode.findAll({ rule: { kind: "class_declaration" } })) { - if (!isReactClass(classDecl)) continue; - const body = classDecl.find({ rule: { kind: "class_body" } }); - if (!body) continue; + for (const body of reactClassBodies(rootNode)) { const edit = replacementFor(body, collectChunks(body, source), methodsOrder, "\n\n "); if (edit) { edits.push(edit); diff --git a/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx b/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx new file mode 100644 index 0000000..7184b23 --- /dev/null +++ b/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx @@ -0,0 +1,9 @@ +var createClass = require('create-react-class'); + +var C = createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx b/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx new file mode 100644 index 0000000..7184b23 --- /dev/null +++ b/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx @@ -0,0 +1,9 @@ +var createClass = require('create-react-class'); + +var C = createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx b/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx new file mode 100644 index 0000000..152ec11 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx @@ -0,0 +1,7 @@ +var C = React.createClass({ + a() {}, + + render() { + return
    ; + }, +}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/input.tsx b/codemods/sort-comp/tests/no-react-explicit-require/input.tsx new file mode 100644 index 0000000..d86a4e7 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-explicit-require/input.tsx @@ -0,0 +1,7 @@ +var C = React.createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/metrics.json b/codemods/sort-comp/tests/no-react-explicit-require/metrics.json new file mode 100644 index 0000000..a24fb9d --- /dev/null +++ b/codemods/sort-comp/tests/no-react-explicit-require/metrics.json @@ -0,0 +1,11 @@ +{ + "sort-comp-reorders": [ + { + "cardinality": { + "file": "tests/no-react-explicit-require/input.tsx", + "kind": "createClass" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/sort-comp/tests/no-react-explicit-require/test.config.json b/codemods/sort-comp/tests/no-react-explicit-require/test.config.json new file mode 100644 index 0000000..4858435 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-explicit-require/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "explicit-require": false + } +} diff --git a/codemods/sort-comp/tests/no-react-no-change/expected.tsx b/codemods/sort-comp/tests/no-react-no-change/expected.tsx new file mode 100644 index 0000000..d86a4e7 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-no-change/expected.tsx @@ -0,0 +1,7 @@ +var C = React.createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/no-react-no-change/input.tsx b/codemods/sort-comp/tests/no-react-no-change/input.tsx new file mode 100644 index 0000000..d86a4e7 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-no-change/input.tsx @@ -0,0 +1,7 @@ +var C = React.createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx b/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx new file mode 100644 index 0000000..97a64fb --- /dev/null +++ b/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx @@ -0,0 +1,9 @@ +import { Component } from 'other-lib'; + +class C extends Component { + render() { + return
    ; + } + + a() {} +} diff --git a/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx b/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx new file mode 100644 index 0000000..97a64fb --- /dev/null +++ b/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx @@ -0,0 +1,9 @@ +import { Component } from 'other-lib'; + +class C extends Component { + render() { + return
    ; + } + + a() {} +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a380164..17dc5f9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,7 +14,19 @@ importers: devDependencies: '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 + + codemods/create-element-to-jsx: + devDependencies: + '@codemod.com/jssg-types': + specifier: latest + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.6.0 + typescript: + specifier: latest + version: 6.0.2 codemods/error-boundaries: devDependencies: @@ -23,7 +35,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -39,7 +51,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -51,7 +63,19 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 + typescript: + specifier: latest + version: 6.0.2 + + codemods/pure-component: + devDependencies: + '@codemod.com/jssg-types': + specifier: latest + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -63,7 +87,39 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 + typescript: + specifier: latest + version: 6.0.2 + + codemods/react-dom-to-react-dom-factories: + dependencies: + '@jssg/utils': + specifier: ^0.0.2 + version: 0.0.2 + devDependencies: + '@codemod.com/jssg-types': + specifier: latest + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.6.0 + typescript: + specifier: latest + version: 6.0.2 + + codemods/react-native-view-prop-types: + dependencies: + '@jssg/utils': + specifier: ^0.0.2 + version: 0.0.2 + devDependencies: + '@codemod.com/jssg-types': + specifier: latest + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -79,7 +135,47 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 + typescript: + specifier: latest + version: 6.0.2 + + codemods/react-to-react-dom: + devDependencies: + '@codemod.com/jssg-types': + specifier: latest + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.6.0 + typescript: + specifier: latest + version: 6.0.2 + + codemods/remove-context-provider: + devDependencies: + '@codemod.com/jssg-types': + specifier: latest + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.6.0 + typescript: + specifier: latest + version: 6.0.2 + + codemods/remove-forward-ref: + dependencies: + '@jssg/utils': + specifier: ^0.0.2 + version: 0.0.2 + devDependencies: + '@codemod.com/jssg-types': + specifier: latest + version: 1.5.1 + '@types/node': + specifier: latest + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -91,7 +187,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -107,7 +203,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -123,7 +219,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -139,7 +235,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -155,7 +251,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -167,7 +263,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -183,7 +279,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -199,7 +295,7 @@ importers: version: 1.5.1 '@types/node': specifier: latest - version: 25.5.2 + version: 25.6.0 typescript: specifier: latest version: 6.0.2 @@ -212,16 +308,16 @@ packages: '@jssg/utils@0.0.2': resolution: {integrity: sha512-eCQv5Xs9yfI6OKq2PQ8SyKsxPhdiaJY/XAFJmsZiVbsK5DIBBmGBA95CsyD4JFWXDKkNv6Q7ER/Kbp6/uWzB2w==} - '@types/node@25.5.2': - resolution: {integrity: sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} typescript@6.0.2: resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} hasBin: true - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} snapshots: @@ -229,10 +325,10 @@ snapshots: '@jssg/utils@0.0.2': {} - '@types/node@25.5.2': + '@types/node@25.6.0': dependencies: - undici-types: 7.18.2 + undici-types: 7.19.2 typescript@6.0.2: {} - undici-types@7.18.2: {} + undici-types@7.19.2: {} From be9fd4bd58f9667ec283e80e1a56253202a8ce62 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 00:41:32 +0200 Subject: [PATCH 10/29] fix find-dom-node --- codemods/find-dom-node/README.md | 2 + codemods/find-dom-node/package.json | 2 +- codemods/find-dom-node/scripts/codemod.ts | 5 - .../find-dom-node/scripts/parity-tests.mjs | 186 ++++++++++++++++++ .../tests/es6-class-no-change/expected.tsx | 7 - .../tests/es6-class-no-change/input.tsx | 7 - .../tests/export-default/expected.tsx | 7 - .../tests/export-default/input.tsx | 7 - .../tests/export-default/metrics.json | 10 - .../tests/helper-no-change/expected.tsx | 2 - .../tests/helper-no-change/input.tsx | 2 - .../tests/module-exports/expected.tsx | 7 - .../tests/module-exports/input.tsx | 7 - .../tests/module-exports/metrics.json | 10 - .../expected.tsx | 5 - .../input.tsx | 5 - .../metrics.json | 10 - .../test.config.json | 5 - .../tests/no-react-create-class/expected.tsx | 5 - .../tests/no-react-create-class/input.tsx | 5 - 20 files changed, 189 insertions(+), 107 deletions(-) create mode 100644 codemods/find-dom-node/scripts/parity-tests.mjs delete mode 100644 codemods/find-dom-node/tests/es6-class-no-change/expected.tsx delete mode 100644 codemods/find-dom-node/tests/es6-class-no-change/input.tsx delete mode 100644 codemods/find-dom-node/tests/export-default/expected.tsx delete mode 100644 codemods/find-dom-node/tests/export-default/input.tsx delete mode 100644 codemods/find-dom-node/tests/export-default/metrics.json delete mode 100644 codemods/find-dom-node/tests/helper-no-change/expected.tsx delete mode 100644 codemods/find-dom-node/tests/helper-no-change/input.tsx delete mode 100644 codemods/find-dom-node/tests/module-exports/expected.tsx delete mode 100644 codemods/find-dom-node/tests/module-exports/input.tsx delete mode 100644 codemods/find-dom-node/tests/module-exports/metrics.json delete mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx delete mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx delete mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json delete mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json delete mode 100644 codemods/find-dom-node/tests/no-react-create-class/expected.tsx delete mode 100644 codemods/find-dom-node/tests/no-react-create-class/input.tsx diff --git a/codemods/find-dom-node/README.md b/codemods/find-dom-node/README.md index 2ffc54d..3a59472 100644 --- a/codemods/find-dom-node/README.md +++ b/codemods/find-dom-node/README.md @@ -18,3 +18,5 @@ codemod run @react/find-dom-node pnpm test pnpm check-types ``` + +The checked-in `tests/` directory preserves the exact legacy fixture surface. Additional rollout-safety parity cases are verified in `scripts/parity-tests.mjs`. diff --git a/codemods/find-dom-node/package.json b/codemods/find-dom-node/package.json index de25953..a082821 100644 --- a/codemods/find-dom-node/package.json +++ b/codemods/find-dom-node/package.json @@ -4,7 +4,7 @@ "description": "Replace getDOMNode() with React.findDOMNode()", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/parity-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/find-dom-node/scripts/codemod.ts b/codemods/find-dom-node/scripts/codemod.ts index 66e8360..b98dc0a 100644 --- a/codemods/find-dom-node/scripts/codemod.ts +++ b/codemods/find-dom-node/scripts/codemod.ts @@ -81,11 +81,6 @@ function createClassContainers(rootNode: SgNode): SgNode[] } } - for (const exportStmt of rootNode.findAll({ rule: { kind: "export_statement" } })) { - const exported = exportStmt.children().find((child) => child.isNamed() && child.kind() === "call_expression"); - if (isReactCreateClassCall(exported ?? null)) containers.push(exportStmt); - } - return containers; } diff --git a/codemods/find-dom-node/scripts/parity-tests.mjs b/codemods/find-dom-node/scripts/parity-tests.mjs new file mode 100644 index 0000000..d805a79 --- /dev/null +++ b/codemods/find-dom-node/scripts/parity-tests.mjs @@ -0,0 +1,186 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); +const packageDir = path.dirname(path.dirname(__filename)); +const workflowPath = path.join(packageDir, "workflow.yaml"); + +function runJssg(source, params = {}) { + const dir = mkdtempSync(path.join(tmpdir(), "find-dom-node-")); + const inputPath = path.join(dir, "input.tsx"); + writeFileSync(inputPath, source, "utf8"); + writeFileSync(path.join(dir, ".gitignore"), "", "utf8"); + + try { + spawnSync("git", ["init", "-q"], { cwd: dir }); + spawnSync("git", ["add", "input.tsx", ".gitignore"], { cwd: dir }); + spawnSync("git", ["commit", "-qm", "init"], { cwd: dir }); + + const args = [ + "dlx", + "codemod@latest", + "workflow", + "run", + "-w", + workflowPath, + "--target", + dir, + "--allow-dirty", + ]; + for (const [key, value] of Object.entries(params)) { + args.push("--param", `${key}=${value}`); + } + + const result = spawnSync("pnpm", args, { + cwd: packageDir, + encoding: "utf8", + timeout: 30000, + }); + assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); + return readFileSync(inputPath, "utf8").trim(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +const parityCases = [ + { + name: "module.exports createClass", + input: `var React = require('React'); + +module.exports = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); +`, + expected: `var React = require('React'); + +module.exports = React.createClass({ + render: function() { + return React.findDOMNode(this); + }, +});`, + }, + { + name: "createClass without React stays unchanged by default", + input: `var Composer = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); +`, + expected: `var Composer = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +});`, + }, + { + name: "createClass without React transforms with explicit-require false", + input: `var Composer = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); +`, + params: { "explicit-require": false }, + expected: `var Composer = React.createClass({ + render: function() { + return React.findDOMNode(this); + }, +});`, + }, + { + name: "es6 classes stay unchanged", + input: `var React = require('React'); + +class C extends React.Component { + render() { + return this.getDOMNode(); + } +} +`, + expected: `var React = require('React'); + +class C extends React.Component { + render() { + return this.getDOMNode(); + } +}`, + }, + { + name: "non-React helpers stay unchanged", + input: `const helper = foo(); +helper.getDOMNode(); +`, + expected: `const helper = foo(); +helper.getDOMNode();`, + }, + { + name: "computed refs alias is transformed", + input: `var React = require('React'); +var C = React.createClass({ + render: function() { + var ref = 'foo'; + var thing = this.refs[ref]; + return thing.getDOMNode(); + } +}); +`, + expected: `var React = require('React'); +var C = React.createClass({ + render: function() { + var ref = 'foo'; + var thing = this.refs[ref]; + return React.findDOMNode(thing); + } +});`, + }, + { + name: "deep refs chain is transformed", + input: `var React = require('React'); +var C = React.createClass({ + render: function() { + return this.refs.main.refs.list.getDOMNode(); + } +}); +`, + expected: `var React = require('React'); +var C = React.createClass({ + render: function() { + return React.findDOMNode(this.refs.main.refs.list); + } +});`, + }, + { + name: "export default createClass remains unchanged", + input: `var React = require('React'); + +export default React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); +`, + expected: `var React = require('React'); + +export default React.createClass({ + render: function() { + return this.getDOMNode(); + }, +});`, + }, +]; + +for (const testCase of parityCases) { + test(`matches parity case: ${testCase.name}`, () => { + const output = runJssg(testCase.input, testCase.params ?? {}); + assert.strictEqual(output, testCase.expected); + }); +} diff --git a/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx b/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx deleted file mode 100644 index 464aee7..0000000 --- a/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -class C extends React.Component { - render() { - return this.getDOMNode(); - } -} diff --git a/codemods/find-dom-node/tests/es6-class-no-change/input.tsx b/codemods/find-dom-node/tests/es6-class-no-change/input.tsx deleted file mode 100644 index 464aee7..0000000 --- a/codemods/find-dom-node/tests/es6-class-no-change/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -class C extends React.Component { - render() { - return this.getDOMNode(); - } -} diff --git a/codemods/find-dom-node/tests/export-default/expected.tsx b/codemods/find-dom-node/tests/export-default/expected.tsx deleted file mode 100644 index ae81e09..0000000 --- a/codemods/find-dom-node/tests/export-default/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -export default React.createClass({ - render: function() { - return React.findDOMNode(this); - }, -}); diff --git a/codemods/find-dom-node/tests/export-default/input.tsx b/codemods/find-dom-node/tests/export-default/input.tsx deleted file mode 100644 index 97fa6eb..0000000 --- a/codemods/find-dom-node/tests/export-default/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -export default React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/find-dom-node/tests/export-default/metrics.json b/codemods/find-dom-node/tests/export-default/metrics.json deleted file mode 100644 index 6d1bd65..0000000 --- a/codemods/find-dom-node/tests/export-default/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/export-default/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/helper-no-change/expected.tsx b/codemods/find-dom-node/tests/helper-no-change/expected.tsx deleted file mode 100644 index 281d7fa..0000000 --- a/codemods/find-dom-node/tests/helper-no-change/expected.tsx +++ /dev/null @@ -1,2 +0,0 @@ -const helper = foo(); -helper.getDOMNode(); diff --git a/codemods/find-dom-node/tests/helper-no-change/input.tsx b/codemods/find-dom-node/tests/helper-no-change/input.tsx deleted file mode 100644 index 281d7fa..0000000 --- a/codemods/find-dom-node/tests/helper-no-change/input.tsx +++ /dev/null @@ -1,2 +0,0 @@ -const helper = foo(); -helper.getDOMNode(); diff --git a/codemods/find-dom-node/tests/module-exports/expected.tsx b/codemods/find-dom-node/tests/module-exports/expected.tsx deleted file mode 100644 index cab9f4c..0000000 --- a/codemods/find-dom-node/tests/module-exports/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -module.exports = React.createClass({ - render: function() { - return React.findDOMNode(this); - }, -}); diff --git a/codemods/find-dom-node/tests/module-exports/input.tsx b/codemods/find-dom-node/tests/module-exports/input.tsx deleted file mode 100644 index 9658cf5..0000000 --- a/codemods/find-dom-node/tests/module-exports/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -module.exports = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/find-dom-node/tests/module-exports/metrics.json b/codemods/find-dom-node/tests/module-exports/metrics.json deleted file mode 100644 index 42a4df1..0000000 --- a/codemods/find-dom-node/tests/module-exports/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/module-exports/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx deleted file mode 100644 index dd2e29f..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx +++ /dev/null @@ -1,5 +0,0 @@ -var Composer = React.createClass({ - render: function() { - return React.findDOMNode(this); - }, -}); diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx deleted file mode 100644 index 8b8bbf5..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -var Composer = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json deleted file mode 100644 index 6b828be..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/no-react-create-class-explicit-require/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json deleted file mode 100644 index 4858435..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "explicit-require": false - } -} diff --git a/codemods/find-dom-node/tests/no-react-create-class/expected.tsx b/codemods/find-dom-node/tests/no-react-create-class/expected.tsx deleted file mode 100644 index 8b8bbf5..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class/expected.tsx +++ /dev/null @@ -1,5 +0,0 @@ -var Composer = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/find-dom-node/tests/no-react-create-class/input.tsx b/codemods/find-dom-node/tests/no-react-create-class/input.tsx deleted file mode 100644 index 8b8bbf5..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -var Composer = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); From 5a00e5f5df13f68296e3479be47843257da455fa Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 01:01:18 +0200 Subject: [PATCH 11/29] fix pure-render-mixin --- codemods/pure-render-mixin/README.md | 2 + codemods/pure-render-mixin/package.json | 2 +- codemods/pure-render-mixin/scripts/codemod.ts | 123 ++++++++++------- .../scripts/parity-tests.mjs | 130 ++++++++++++++++++ .../expected.tsx | 11 -- .../input.tsx | 11 -- .../no-react-explicit-require/expected.tsx | 12 -- .../tests/no-react-explicit-require/input.tsx | 9 -- .../no-react-explicit-require/metrics.json | 10 -- .../test.config.json | 5 - .../tests/no-react-no-change/expected.tsx | 9 -- .../tests/no-react-no-change/input.tsx | 9 -- .../tests/pure-render-mixin/expected.tsx | 6 - .../tests/pure-render-mixin2/expected.tsx | 3 - .../tests/pure-render-mixin3/expected.tsx | 4 +- .../tests/pure-render-mixin4/expected.tsx | 4 +- 16 files changed, 205 insertions(+), 145 deletions(-) create mode 100644 codemods/pure-render-mixin/scripts/parity-tests.mjs delete mode 100644 codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx delete mode 100644 codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx delete mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx delete mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx delete mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json delete mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json delete mode 100644 codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx delete mode 100644 codemods/pure-render-mixin/tests/no-react-no-change/input.tsx diff --git a/codemods/pure-render-mixin/README.md b/codemods/pure-render-mixin/README.md index 847c05c..a40a0af 100644 --- a/codemods/pure-render-mixin/README.md +++ b/codemods/pure-render-mixin/README.md @@ -19,3 +19,5 @@ codemod run @react/pure-render-mixin pnpm test pnpm check-types ``` + +The checked-in `tests/` directory preserves the exact legacy fixture surface. Additional rollout-safety parity cases are verified in `scripts/parity-tests.mjs`. diff --git a/codemods/pure-render-mixin/package.json b/codemods/pure-render-mixin/package.json index 83daaf7..fa5f796 100644 --- a/codemods/pure-render-mixin/package.json +++ b/codemods/pure-render-mixin/package.json @@ -4,7 +4,7 @@ "description": "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/parity-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/pure-render-mixin/scripts/codemod.ts b/codemods/pure-render-mixin/scripts/codemod.ts index 8107706..bf6180d 100644 --- a/codemods/pure-render-mixin/scripts/codemod.ts +++ b/codemods/pure-render-mixin/scripts/codemod.ts @@ -108,6 +108,70 @@ function getPairKeyName(node: SgNode): string { return node.field("key")?.text() ?? node.field("name")?.text() ?? ""; } +function declarationRemovalRange(varDecl: SgNode, source: string): { start: number; end: number } { + let start = varDecl.range().start.index; + let end = varDecl.range().end.index; + const before = source.slice(0, start); + const after = source.slice(end); + if (after.startsWith("\n\n")) { + end += 1; + } + if (before.endsWith("\n\n") && after.startsWith("\n\n")) { + start -= 1; + } + return { start, end }; +} + +function shouldUpdatePropertyText(): string { + return `${SHOULD_COMPONENT_UPDATE}: function(nextProps, nextState) {\n return React.addons.shallowCompare(this, nextProps, nextState);\n }`; +} + +function renderConfigObject(configObj: SgNode, mixinsPair: SgNode, mixinName: string): string | null { + const mixinsValue = mixinsPair.field("value"); + if (!mixinsValue || mixinsValue.kind() !== "array") return null; + const mixinInArray = mixinsValue.findAll(arrayIdentifierNamedRule(mixinName)); + if (mixinInArray.length === 0) return null; + + const toRemoveSet = new Set(mixinInArray.map((n) => n.range().start.index)); + const kept = mixinsValue.children().filter( + (c) => + c.kind() !== "[" && + c.kind() !== "]" && + c.kind() !== "," && + !toRemoveSet.has(c.range().start.index), + ); + + const pairs = configObj.findAll(PAIR_RULE) + .sort((a, b) => a.range().start.index - b.range().start.index); + const renderedProps: string[] = []; + let renderIndex = -1; + + for (const pair of pairs) { + if (pair.id() === mixinsPair.id()) { + if (kept.length > 0) { + renderedProps.push(`mixins: [${kept.map((e) => e.text()).join(", ")}]`); + } + continue; + } + + if (getPairKeyName(pair) === RENDER) { + renderIndex = renderedProps.length; + } + renderedProps.push(pair.text()); + } + + if (renderedProps.length === 0) return null; + + const shouldUpdateText = shouldUpdatePropertyText(); + if (renderIndex >= 0 && renderIndex === renderedProps.length - 1) { + renderedProps.splice(renderIndex, 0, shouldUpdateText); + } else { + renderedProps.push(shouldUpdateText); + } + + return `{\n ${renderedProps.join(",\n\n ")},\n}`; +} + const transform: Transform = async (root, options) => { const rootNode = root.root(); if (!explicitRequireDisabled(options.params?.["explicit-require"]) && !hasReact(rootNode)) { @@ -136,56 +200,9 @@ const transform: Transform = async (root, options) => { const mixinInArray = value.findAll(arrayIdentifierNamedRule(mixinName)); if (mixinInArray.length === 0) continue; - const toRemoveSet = new Set(mixinInArray.map((n) => n.range().start.index)); - const kept = value.children().filter( - (c) => - c.kind() !== "[" && - c.kind() !== "]" && - c.kind() !== "," && - !toRemoveSet.has(c.range().start.index), - ); - const newArrayText = kept.length === 0 ? "[]" : "[" + kept.map((e) => e.text()).join(", ") + "]"; - - const allPairs = configObj.findAll(PAIR_RULE); - allPairs.sort((a, b) => a.range().start.index - b.range().start.index); - const mixinsIdx = allPairs.findIndex((p) => p.range().start.index === mixinsPair.range().start.index); - const nextPair = mixinsIdx >= 0 && mixinsIdx < allPairs.length - 1 ? allPairs[mixinsIdx + 1]! : null; - const prevPair = mixinsIdx > 0 ? allPairs[mixinsIdx - 1]! : null; - - if (newArrayText === "[]") { - const removeStart = nextPair - ? mixinsPair.range().start.index - : prevPair - ? prevPair.range().end.index - : mixinsPair.range().start.index; - const removeEnd = nextPair - ? nextPair.range().start.index - : mixinsPair.range().end.index; - edits.push({ - startPos: removeStart, - endPos: removeEnd, - insertedText: "", - }); - } else { - const newMixinsText = `mixins: ${newArrayText}`; - edits.push(mixinsPair.replace(newMixinsText)); - } - - const allPairsByPosition = [...allPairs].sort((a, b) => a.range().start.index - b.range().start.index); - const lastPair = allPairsByPosition[allPairsByPosition.length - 1]!; - const insertBeforeRender = getPairKeyName(lastPair) === RENDER; - const insertPos = insertBeforeRender ? renderPair.range().start.index : lastPair.range().end.index; - const beforeInsert = source.slice(Math.max(0, insertPos - 30), insertPos); - const indentMatch = beforeInsert.match(/\n(\s*)$/); - const indent = indentMatch ? indentMatch[1] : " "; - const shouldUpdateBlock = insertBeforeRender - ? `${indent}${SHOULD_COMPONENT_UPDATE}: function(nextProps, nextState) {\n${indent} return React.addons.shallowCompare(this, nextProps, nextState);\n${indent}},\n${indent}` - : `,\n\n${indent}${SHOULD_COMPONENT_UPDATE}: function(nextProps, nextState) {\n${indent} return React.addons.shallowCompare(this, nextProps, nextState);\n${indent}}`; - edits.push({ - startPos: insertPos, - endPos: insertPos, - insertedText: shouldUpdateBlock, - }); + const newConfigText = renderConfigObject(configObj, mixinsPair, mixinName); + if (!newConfigText) continue; + edits.push(configObj.replace(newConfigText)); transformCount++; metric.increment({ file: metricFile(root.filename()) }); @@ -209,10 +226,10 @@ const transform: Transform = async (root, options) => { const decls = varDecl.findAll({ rule: { kind: "variable_declarator" } }); if (decls.length !== 1) continue; const after = source.slice(varDecl.range().end.index, varDecl.range().end.index + 10); - const trailing = after.match(/^(\s*\n?)/)?.[1] ?? "\n"; + const range = declarationRemovalRange(varDecl, source); edits.push({ - startPos: varDecl.range().start.index, - endPos: varDecl.range().end.index + trailing.length, + startPos: range.start, + endPos: range.end, insertedText: "", }); break; diff --git a/codemods/pure-render-mixin/scripts/parity-tests.mjs b/codemods/pure-render-mixin/scripts/parity-tests.mjs new file mode 100644 index 0000000..07c73d2 --- /dev/null +++ b/codemods/pure-render-mixin/scripts/parity-tests.mjs @@ -0,0 +1,130 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); +const packageDir = path.dirname(path.dirname(__filename)); +const workflowPath = path.join(packageDir, "workflow.yaml"); + +function runJssg(source, params = {}) { + const dir = mkdtempSync(path.join(tmpdir(), "pure-render-mixin-")); + const inputPath = path.join(dir, "input.tsx"); + writeFileSync(inputPath, source, "utf8"); + writeFileSync(path.join(dir, ".gitignore"), "", "utf8"); + + try { + spawnSync("git", ["init", "-q"], { cwd: dir }); + spawnSync("git", ["add", "input.tsx", ".gitignore"], { cwd: dir }); + spawnSync("git", ["commit", "-qm", "init"], { cwd: dir }); + + const args = [ + "dlx", + "codemod@latest", + "workflow", + "run", + "-w", + workflowPath, + "--target", + dir, + "--allow-dirty", + ]; + for (const [key, value] of Object.entries(params)) { + args.push("--param", `${key}=${value}`); + } + + const result = spawnSync("pnpm", args, { + cwd: packageDir, + encoding: "utf8", + timeout: 30000, + }); + assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); + return readFileSync(inputPath, "utf8").trim(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +const parityCases = [ + { + name: "createClass without React stays unchanged by default", + input: `var PureRenderMixin = React.addons.PureRenderMixin; + +var C = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); +`, + expected: `var PureRenderMixin = React.addons.PureRenderMixin; + +var C = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +});`, + }, + { + name: "createClass without React transforms with explicit-require false", + input: `var PureRenderMixin = React.addons.PureRenderMixin; + +var C = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); +`, + params: { "explicit-require": false }, + expected: `var C = React.createClass({ + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + }, + + render: function() { + return
    ; + }, +});`, + }, + { + name: "identifier createClass stays unchanged", + input: `var React = require('react/addons'); +var PureRenderMixin = React.addons.PureRenderMixin; +var createClass = require('create-react-class'); + +var C = createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); +`, + expected: `var React = require('react/addons'); +var PureRenderMixin = React.addons.PureRenderMixin; +var createClass = require('create-react-class'); + +var C = createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +});`, + }, +]; + +for (const testCase of parityCases) { + test(`matches parity case: ${testCase.name}`, () => { + const output = runJssg(testCase.input, testCase.params ?? {}); + assert.strictEqual(output, testCase.expected); + }); +} diff --git a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx deleted file mode 100644 index bd81dcc..0000000 --- a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; -var createClass = require('create-react-class'); - -var C = createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx deleted file mode 100644 index bd81dcc..0000000 --- a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; -var createClass = require('create-react-class'); - -var C = createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx b/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx deleted file mode 100644 index c0d6447..0000000 --- a/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx +++ /dev/null @@ -1,12 +0,0 @@ -var C = React.createClass({ - - shouldComponentUpdate: function(nextProps, nextState) { - - return React.addons.shallowCompare(this, nextProps, nextState); - - }, - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx b/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx deleted file mode 100644 index 13cd748..0000000 --- a/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var PureRenderMixin = React.addons.PureRenderMixin; - -var C = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json b/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json deleted file mode 100644 index fd0f9ae..0000000 --- a/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/no-react-explicit-require/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json b/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json deleted file mode 100644 index 4858435..0000000 --- a/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "explicit-require": false - } -} diff --git a/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx b/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx deleted file mode 100644 index 13cd748..0000000 --- a/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var PureRenderMixin = React.addons.PureRenderMixin; - -var C = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx b/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx deleted file mode 100644 index 13cd748..0000000 --- a/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var PureRenderMixin = React.addons.PureRenderMixin; - -var C = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin/expected.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin/expected.tsx index a6a8ded..7d7ac3d 100644 --- a/codemods/pure-render-mixin/tests/pure-render-mixin/expected.tsx +++ b/codemods/pure-render-mixin/tests/pure-render-mixin/expected.tsx @@ -3,11 +3,8 @@ var React = require('react/addons'); var PureRenderMixin = React.addons.PureRenderMixin; var MyComponent = React.createClass({ - shouldComponentUpdate: function(nextProps, nextState) { - return React.addons.shallowCompare(this, nextProps, nextState); - }, render: function() { @@ -18,11 +15,8 @@ var MyComponent = React.createClass({ var MyMixedComponent = React.createClass({ mixins: [SomeOtherMixin], - shouldComponentUpdate: function(nextProps, nextState) { - return React.addons.shallowCompare(this, nextProps, nextState); - }, render: function() { diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin2/expected.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin2/expected.tsx index 88bd5aa..26425d8 100644 --- a/codemods/pure-render-mixin/tests/pure-render-mixin2/expected.tsx +++ b/codemods/pure-render-mixin/tests/pure-render-mixin2/expected.tsx @@ -1,11 +1,8 @@ var React = require('react/addons'); var MyComponent = React.createClass({ - shouldComponentUpdate: function(nextProps, nextState) { - return React.addons.shallowCompare(this, nextProps, nextState); - }, render: function() { diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin3/expected.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin3/expected.tsx index 659d0a9..cd97493 100644 --- a/codemods/pure-render-mixin/tests/pure-render-mixin3/expected.tsx +++ b/codemods/pure-render-mixin/tests/pure-render-mixin3/expected.tsx @@ -1,12 +1,10 @@ var React = require('react/addons'); var Foo = 'Foo'; + var MyComponent = React.createClass({ - shouldComponentUpdate: function(nextProps, nextState) { - return React.addons.shallowCompare(this, nextProps, nextState); - }, render: function() { diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin4/expected.tsx b/codemods/pure-render-mixin/tests/pure-render-mixin4/expected.tsx index 71bcd04..3e3854e 100644 --- a/codemods/pure-render-mixin/tests/pure-render-mixin4/expected.tsx +++ b/codemods/pure-render-mixin/tests/pure-render-mixin4/expected.tsx @@ -1,10 +1,8 @@ var React = require('React'); + var MyComponent = React.createClass({ - shouldComponentUpdate: function(nextProps, nextState) { - return React.addons.shallowCompare(this, nextProps, nextState); - }, render: function() { From 362e0c3cb6df2bc7131d37cbcfa382b56f8fcce4 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 01:31:31 +0200 Subject: [PATCH 12/29] improve sort-comp --- codemods/sort-comp/README.md | 4 + codemods/sort-comp/codemod.yaml | 3 +- codemods/sort-comp/package.json | 6 +- codemods/sort-comp/scripts/codemod.ts | 314 +++++++++++++++++- codemods/sort-comp/scripts/parity-tests.mjs | 173 ++++++++++ .../custom-sort-custom-sort/test.config.json | 10 - .../metrics.json | 11 - .../test.config.json | 11 - .../tests/custom-sort-group/.eslintrc | 19 ++ .../expected.tsx | 0 .../input.tsx | 0 .../metrics.json | 2 +- .../sort-comp/tests/custom-sort/.eslintrc | 12 + .../expected.tsx | 0 .../input.tsx | 0 .../metrics.json | 2 +- .../expected.tsx | 9 - .../input.tsx | 9 - .../no-react-explicit-require/expected.tsx | 7 - .../tests/no-react-explicit-require/input.tsx | 7 - .../test.config.json | 5 - .../tests/no-react-no-change/expected.tsx | 7 - .../tests/no-react-no-change/input.tsx | 7 - .../expected.tsx | 9 - .../unrelated-component-no-change/input.tsx | 9 - 25 files changed, 521 insertions(+), 115 deletions(-) create mode 100644 codemods/sort-comp/scripts/parity-tests.mjs delete mode 100644 codemods/sort-comp/tests/custom-sort-custom-sort/test.config.json delete mode 100644 codemods/sort-comp/tests/custom-sort-group-custom-sort-group/metrics.json delete mode 100644 codemods/sort-comp/tests/custom-sort-group-custom-sort-group/test.config.json create mode 100644 codemods/sort-comp/tests/custom-sort-group/.eslintrc rename codemods/sort-comp/tests/{custom-sort-group-custom-sort-group => custom-sort-group}/expected.tsx (100%) rename codemods/sort-comp/tests/{custom-sort-group-custom-sort-group => custom-sort-group}/input.tsx (100%) rename codemods/sort-comp/tests/{custom-sort-custom-sort => custom-sort-group}/metrics.json (67%) create mode 100644 codemods/sort-comp/tests/custom-sort/.eslintrc rename codemods/sort-comp/tests/{custom-sort-custom-sort => custom-sort}/expected.tsx (100%) rename codemods/sort-comp/tests/{custom-sort-custom-sort => custom-sort}/input.tsx (100%) rename codemods/sort-comp/tests/{no-react-explicit-require => custom-sort}/metrics.json (67%) delete mode 100644 codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx delete mode 100644 codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx delete mode 100644 codemods/sort-comp/tests/no-react-explicit-require/expected.tsx delete mode 100644 codemods/sort-comp/tests/no-react-explicit-require/input.tsx delete mode 100644 codemods/sort-comp/tests/no-react-explicit-require/test.config.json delete mode 100644 codemods/sort-comp/tests/no-react-no-change/expected.tsx delete mode 100644 codemods/sort-comp/tests/no-react-no-change/input.tsx delete mode 100644 codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx delete mode 100644 codemods/sort-comp/tests/unrelated-component-no-change/input.tsx diff --git a/codemods/sort-comp/README.md b/codemods/sort-comp/README.md index 0457d52..04b52c8 100644 --- a/codemods/sort-comp/README.md +++ b/codemods/sort-comp/README.md @@ -19,3 +19,7 @@ codemod run @react/sort-comp pnpm test pnpm check-types ``` + +The checked-in `tests/` directory preserves the exact legacy fixture surface. Additional rollout-safety parity cases are verified in `scripts/parity-tests.mjs`. + +The remaining blocker is full parity for shareable / fully resolved ESLint config resolution. The legacy codemod delegated that to ESLint’s full config machinery. Inside JSSG, I implemented a clean classic-config reader that covers the legacy fixture cases and direct local config branches, but I did not ship a heavier config-execution workaround for every ESLint extends shape. \ No newline at end of file diff --git a/codemods/sort-comp/codemod.yaml b/codemods/sort-comp/codemod.yaml index 673f8c4..04eb095 100644 --- a/codemods/sort-comp/codemod.yaml +++ b/codemods/sort-comp/codemod.yaml @@ -16,4 +16,5 @@ registry: access: "public" visibility: "public" -capabilities: [] +capabilities: + - fs diff --git a/codemods/sort-comp/package.json b/codemods/sort-comp/package.json index a906c44..c4edffd 100644 --- a/codemods/sort-comp/package.json +++ b/codemods/sort-comp/package.json @@ -4,12 +4,12 @@ "description": "Reorder React component methods to match eslint-plugin-react sort-comp rule", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/parity-tests.mjs", "check-types": "tsc --noEmit" }, "devDependencies": { "@codemod.com/jssg-types": "latest", - "typescript": "latest", - "@types/node": "latest" + "@types/node": "latest", + "typescript": "latest" } } diff --git a/codemods/sort-comp/scripts/codemod.ts b/codemods/sort-comp/scripts/codemod.ts index 5b7a501..0ecab8a 100644 --- a/codemods/sort-comp/scripts/codemod.ts +++ b/codemods/sort-comp/scripts/codemod.ts @@ -1,6 +1,9 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; +import * as fs from "fs"; +import path from "path"; +import { pathToFileURL } from "url"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -190,6 +193,301 @@ const DEFAULT_METHODS_ORDER = [ "render", ]; +type SortCompRuleConfig = { + order?: string[]; + groups?: Record; +}; + +const ESLINT_CONFIG_FILES = [ + ".eslintrc", + ".eslintrc.json", + ".eslintrc.yaml", + ".eslintrc.yml", + ".eslintrc.js", + ".eslintrc.cjs", + ".eslintrc.mjs", + "package.json", +]; + +function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every((entry) => typeof entry === "string"); +} + +function stringArrayParam(value: unknown): string[] | null { + if (isStringArray(value)) return value; + if (typeof value !== "string") return null; + try { + const parsed = JSON.parse(value) as unknown; + return isStringArray(parsed) ? parsed : null; + } catch { + return null; + } +} + +function isGroupsMap(value: unknown): value is Record { + return typeof value === "object" && + value !== null && + Object.values(value).every((entry) => isStringArray(entry)); +} + +function normalizeSortCompRuleConfig(value: unknown): SortCompRuleConfig | null { + if (typeof value !== "object" || value === null) return null; + const maybeConfig = value as { order?: unknown; groups?: unknown }; + const config: SortCompRuleConfig = {}; + if (maybeConfig.order !== undefined) { + if (!isStringArray(maybeConfig.order)) return null; + config.order = maybeConfig.order; + } + if (maybeConfig.groups !== undefined) { + if (!isGroupsMap(maybeConfig.groups)) return null; + config.groups = maybeConfig.groups; + } + return config; +} + +async function getMethodsOrderFromEslint(filePath: string): Promise { + let dir = path.dirname(filePath); + while (true) { + for (const configName of ESLINT_CONFIG_FILES) { + const configPath = path.join(dir, configName); + const methodsOrder = await methodsOrderFromConfigFile(configPath, new Set()); + if (methodsOrder) return methodsOrder; + } + const parent = path.dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + +function resolveMethodsOrder(ruleConfig: SortCompRuleConfig): string[] | null { + if (!ruleConfig.order) return null; + const groups = ruleConfig.groups ?? {}; + const resolvedOrder: string[] = []; + for (const entry of ruleConfig.order) { + const groupEntries = groups[entry]; + if (groupEntries) { + resolvedOrder.push(...groupEntries); + } else { + resolvedOrder.push(entry); + } + } + return resolvedOrder; +} + +function yamlRuleConfig(text: string): SortCompRuleConfig | null { + const lines = text.split("\n"); + const sortCompIndex = lines.findIndex((line) => line.trim() === "react/sort-comp:"); + if (sortCompIndex === -1) return null; + + const ruleConfig: SortCompRuleConfig = {}; + let mode: "order" | "groups" | "group-items" | null = null; + let currentGroup: string | null = null; + + for (let i = sortCompIndex + 1; i < lines.length; i++) { + const line = lines[i]!; + const trimmed = line.trim(); + if (trimmed === "") continue; + const indent = line.match(/^ */)?.[0].length ?? 0; + if (indent <= 2) break; + if (trimmed === "- 0" || trimmed === "- off" || trimmed === "- warn" || trimmed === "- error") continue; + + if (trimmed === "- order:" || trimmed === "order:") { + ruleConfig.order = []; + mode = "order"; + currentGroup = null; + continue; + } + + if (trimmed === "groups:" || trimmed === "- groups:") { + ruleConfig.groups = {}; + mode = "groups"; + currentGroup = null; + continue; + } + + if (mode === "order" && trimmed.startsWith("- ")) { + ruleConfig.order?.push(trimmed.slice(2)); + continue; + } + + if ((mode === "groups" || mode === "group-items") && trimmed.endsWith(":") && !trimmed.startsWith("- ")) { + currentGroup = trimmed.slice(0, -1); + ruleConfig.groups ??= {}; + ruleConfig.groups[currentGroup] = []; + mode = "group-items"; + continue; + } + + if (mode === "group-items" && currentGroup && trimmed.startsWith("- ")) { + ruleConfig.groups?.[currentGroup]?.push(trimmed.slice(2)); + continue; + } + } + + return ruleConfig.order ? ruleConfig : null; +} + +async function loadConfigFile(configPath: string): Promise { + const basename = path.basename(configPath); + if (basename === "package.json") { + try { + const parsed = JSON.parse(fs.readFileSync(configPath, "utf8")) as { eslintConfig?: unknown }; + return parsed.eslintConfig ?? null; + } catch { + return null; + } + } + + if (configPath.endsWith(".js") || configPath.endsWith(".cjs") || configPath.endsWith(".mjs")) { + try { + const imported = await import(pathToFileURL(configPath).href); + return ("default" in imported ? imported.default : imported) as unknown; + } catch { + return null; + } + } + + let text: string; + try { + text = fs.readFileSync(configPath, "utf8"); + } catch { + return null; + } + const trimmed = text.trimStart(); + if (trimmed.startsWith("{")) { + try { + return JSON.parse(text) as unknown; + } catch { + return null; + } + } + + return { rules: { "react/sort-comp": [0, yamlRuleConfig(text)] } }; +} + +function directRuleConfig(config: unknown): SortCompRuleConfig | null { + if (typeof config !== "object" || config === null) return null; + const maybeRules = (config as { rules?: unknown }).rules; + if (typeof maybeRules !== "object" || maybeRules === null) return null; + const sortCompRule = (maybeRules as Record)["react/sort-comp"]; + if (!Array.isArray(sortCompRule) || sortCompRule.length < 2) return null; + return normalizeSortCompRuleConfig(sortCompRule[1]); +} + +function normalizeExtends(value: unknown): string[] { + if (typeof value === "string") return [value]; + return isStringArray(value) ? value : []; +} + +function resolveRelativeExtend(baseDir: string, specifier: string): string | null { + if (!specifier.startsWith(".") && !path.isAbsolute(specifier)) return null; + const candidates = [ + specifier, + `${specifier}.js`, + `${specifier}.cjs`, + `${specifier}.mjs`, + `${specifier}.json`, + `${specifier}.yaml`, + `${specifier}.yml`, + ].map((candidate) => path.resolve(baseDir, candidate)); + for (const candidate of candidates) { + try { + fs.readFileSync(candidate, "utf8"); + return candidate; + } catch { + continue; + } + } + return null; +} + +function tryReadText(filePath: string): string | null { + try { + return fs.readFileSync(filePath, "utf8"); + } catch { + return null; + } +} + +function resolvePackageExtend(baseDir: string, specifier: string): string | null { + if (specifier.startsWith(".") || path.isAbsolute(specifier) || specifier.startsWith("eslint:") || specifier.startsWith("plugin:")) { + return null; + } + + const parts = specifier.split("/"); + const isScoped = specifier.startsWith("@"); + const packagePartLength = isScoped ? 2 : 1; + const packageName = parts.slice(0, packagePartLength).join("/"); + const subpath = parts.slice(packagePartLength).join("/"); + const packageCandidates = (() => { + if (isScoped) { + const [scope, name] = packageName.split("/"); + return name.startsWith("eslint-config-") + ? [packageName] + : [packageName, `${scope}/eslint-config-${name}`]; + } + return packageName.startsWith("eslint-config-") + ? [packageName] + : [packageName, `eslint-config-${packageName}`]; + })(); + + let dir = baseDir; + while (true) { + for (const packageCandidate of packageCandidates) { + const packageRoot = path.join(dir, "node_modules", packageCandidate); + const directCandidate = subpath + ? resolveRelativeExtend(packageRoot, subpath) + : null; + if (directCandidate) return directCandidate; + + const packageJsonPath = path.join(packageRoot, "package.json"); + const packageJsonText = tryReadText(packageJsonPath); + if (packageJsonText) { + try { + const pkg = JSON.parse(packageJsonText) as { main?: string }; + const mainField = typeof pkg.main === "string" ? pkg.main : "index.js"; + const mainCandidate = resolveRelativeExtend(packageRoot, mainField); + if (mainCandidate) return mainCandidate; + } catch { + // ignore malformed package json + } + } + + const indexCandidate = resolveRelativeExtend(packageRoot, "index"); + if (indexCandidate) return indexCandidate; + } + + const parent = path.dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + +async function methodsOrderFromConfigFile( + configPath: string, + seen: Set, +): Promise { + if (seen.has(configPath)) return null; + seen.add(configPath); + + const config = await loadConfigFile(configPath); + const direct = directRuleConfig(config); + const resolvedDirect = direct ? resolveMethodsOrder(direct) : null; + if (resolvedDirect) return resolvedDirect; + + if (typeof config !== "object" || config === null) return null; + const extendsEntries = normalizeExtends((config as { extends?: unknown }).extends); + for (const entry of extendsEntries) { + const resolvedPath = resolveRelativeExtend(path.dirname(configPath), entry) ?? + resolvePackageExtend(path.dirname(configPath), entry); + if (!resolvedPath) continue; + const resolvedOrder = await methodsOrderFromConfigFile(resolvedPath, seen); + if (resolvedOrder) return resolvedOrder; + } + + return null; +} + function isTypeAnnotationMember(node: SgNode): boolean { return node.kind() === "public_field_definition" && node.field("value") === null && @@ -227,10 +525,11 @@ function correctIndex(order: string[], chunk: MemberChunk): number { } function memberName(node: SgNode): string { - if (node.kind() === "method_definition" || node.kind() === "public_field_definition") { - return node.field("name")?.text() ?? ""; - } - return node.field("key")?.text() ?? ""; + const fieldName = node.field("name")?.text() ?? node.field("key")?.text(); + if (fieldName) return fieldName; + return node.children().find((child) => + child.kind() === "property_identifier" || child.kind() === "identifier" + )?.text() ?? ""; } function significantChildren(container: SgNode): SgNode[] { @@ -300,10 +599,9 @@ const transform: Transform = async (root, options) => { const source = rootNode.text(); const edits: Edit[] = []; const metric = useMetricAtom("sort-comp-reorders"); - const methodsOrderParam = options.params?.methodsOrder as unknown; - const methodsOrder = Array.isArray(methodsOrderParam) - ? methodsOrderParam.filter((item): item is string => typeof item === "string") - : DEFAULT_METHODS_ORDER; + const methodsOrder = stringArrayParam(options.params?.methodsOrder) ?? + await getMethodsOrderFromEslint(root.filename()) ?? + DEFAULT_METHODS_ORDER; for (const config of reactCreateClassConfigs(rootNode)) { const edit = replacementFor(config, collectChunks(config, source), methodsOrder, ",\n\n "); diff --git a/codemods/sort-comp/scripts/parity-tests.mjs b/codemods/sort-comp/scripts/parity-tests.mjs new file mode 100644 index 0000000..c8326a8 --- /dev/null +++ b/codemods/sort-comp/scripts/parity-tests.mjs @@ -0,0 +1,173 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); +const packageDir = path.dirname(path.dirname(__filename)); +const codemodFile = path.join(packageDir, "scripts/codemod.ts"); + +function writeExtraFiles(root, files) { + for (const file of files) { + const fullPath = path.join(root, file.name); + mkdirSync(path.dirname(fullPath), { recursive: true }); + writeFileSync(fullPath, file.content, "utf8"); + } +} + +function runJssgWithFiles(source, params = {}, extraFiles = []) { + const root = mkdtempSync(path.join(tmpdir(), "sort-comp-")); + const caseDir = path.join(root, "case"); + mkdirSync(caseDir); + const inputPath = path.join(caseDir, "input.tsx"); + writeFileSync(inputPath, source, "utf8"); + writeFileSync(path.join(caseDir, "expected.tsx"), source, "utf8"); + writeExtraFiles(root, extraFiles); + + try { + if (Object.keys(params).length > 0) { + writeFileSync(path.join(caseDir, "test.config.json"), JSON.stringify({ params }, null, 2)); + } + const result = spawnSync("pnpm", [ + "dlx", + "codemod@latest", + "jssg", + "test", + "-l", + "tsx", + codemodFile, + root, + "-u", + ], { + cwd: packageDir, + encoding: "utf8", + timeout: 30000, + }); + assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); + return readFileSync(path.join(caseDir, "expected.tsx"), "utf8").trim(); + } finally { + rmSync(root, { recursive: true, force: true }); + } +} + +const parityCases = [ + { + name: "createClass without React stays unchanged by default", + input: `var C = React.createClass({ + render() { + return
    ; + }, + + a() {}, +}); +`, + expected: `var C = React.createClass({ + render() { + return
    ; + }, + + a() {}, +});`, + }, + { + name: "createClass without React sorts with explicit-require false", + input: `var C = React.createClass({ + render() { + return
    ; + }, + + a() {}, +}); +`, + params: { "explicit-require": false }, + expected: `var C = React.createClass({ + a() {}, + + render() { + return
    ; + }, +});`, + }, + { + name: "identifier createClass stays unchanged", + input: `var createClass = require('create-react-class'); + +var C = createClass({ + render() { + return
    ; + }, + + a() {}, +}); +`, + expected: `var createClass = require('create-react-class'); + +var C = createClass({ + render() { + return
    ; + }, + + a() {}, +});`, + }, + { + name: "unrelated component stays unchanged", + input: `import { Component } from 'other-lib'; + +class C extends Component { + render() { + return
    ; + } + + a() {} +} +`, + expected: `import { Component } from 'other-lib'; + +class C extends Component { + render() { + return
    ; + } + + a() {} +}`, + }, + { + name: "methodsOrder option overrides built-in order", + input: `var React = require('react/addons'); + +var C = React.createClass({ + render() { + return
    ; + }, + + z() {}, + + componentDidMount() {}, +}); +`, + params: { methodsOrder: JSON.stringify(["componentDidMount", "everything-else", "render"]) }, + expected: `var React = require('react/addons'); + +var C = React.createClass({ + componentDidMount() {}, + + z() {}, + + render() { + return
    ; + }, +});`, + }, +]; + +for (const testCase of parityCases) { + test(`matches parity case: ${testCase.name}`, () => { + const params = { ...(testCase.params ?? {}) }; + const output = runJssgWithFiles(testCase.input, params, testCase.extraFiles ?? []); + assert.strictEqual(output, testCase.expected); + }); +} diff --git a/codemods/sort-comp/tests/custom-sort-custom-sort/test.config.json b/codemods/sort-comp/tests/custom-sort-custom-sort/test.config.json deleted file mode 100644 index 73a66bb..0000000 --- a/codemods/sort-comp/tests/custom-sort-custom-sort/test.config.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "params": { - "methodsOrder": [ - "componentDidMount", - "mixins", - "everything-else", - "render" - ] - } -} diff --git a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/metrics.json b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/metrics.json deleted file mode 100644 index 03cf5fc..0000000 --- a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/custom-sort-group-custom-sort-group/input.tsx", - "kind": "createClass" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/test.config.json b/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/test.config.json deleted file mode 100644 index 6609ce6..0000000 --- a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/test.config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "params": { - "methodsOrder": [ - "componentDidMount", - "mixins", - "everything-else", - "/^render.+$/", - "render" - ] - } -} diff --git a/codemods/sort-comp/tests/custom-sort-group/.eslintrc b/codemods/sort-comp/tests/custom-sort-group/.eslintrc new file mode 100644 index 0000000..5996239 --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort-group/.eslintrc @@ -0,0 +1,19 @@ +--- +parser: babel-eslint +plugins: + - react +rules: + no-undef: 0 + no-unused-vars: 0 + react/sort-comp: + - 0 + - order: + - lifecycle + - everything-else + - rendering + groups: + lifecycle: + - componentDidMount + rendering: + - "/^render.+$/" + - render diff --git a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/expected.tsx b/codemods/sort-comp/tests/custom-sort-group/expected.tsx similarity index 100% rename from codemods/sort-comp/tests/custom-sort-group-custom-sort-group/expected.tsx rename to codemods/sort-comp/tests/custom-sort-group/expected.tsx diff --git a/codemods/sort-comp/tests/custom-sort-group-custom-sort-group/input.tsx b/codemods/sort-comp/tests/custom-sort-group/input.tsx similarity index 100% rename from codemods/sort-comp/tests/custom-sort-group-custom-sort-group/input.tsx rename to codemods/sort-comp/tests/custom-sort-group/input.tsx diff --git a/codemods/sort-comp/tests/custom-sort-custom-sort/metrics.json b/codemods/sort-comp/tests/custom-sort-group/metrics.json similarity index 67% rename from codemods/sort-comp/tests/custom-sort-custom-sort/metrics.json rename to codemods/sort-comp/tests/custom-sort-group/metrics.json index 0149839..ee86a13 100644 --- a/codemods/sort-comp/tests/custom-sort-custom-sort/metrics.json +++ b/codemods/sort-comp/tests/custom-sort-group/metrics.json @@ -2,7 +2,7 @@ "sort-comp-reorders": [ { "cardinality": { - "file": "tests/custom-sort-custom-sort/input.tsx", + "file": "tests/custom-sort-group/input.tsx", "kind": "createClass" }, "count": 1 diff --git a/codemods/sort-comp/tests/custom-sort/.eslintrc b/codemods/sort-comp/tests/custom-sort/.eslintrc new file mode 100644 index 0000000..ab6e1bd --- /dev/null +++ b/codemods/sort-comp/tests/custom-sort/.eslintrc @@ -0,0 +1,12 @@ +--- +parser: babel-eslint +plugins: + - react +rules: + no-undef: 0 + no-unused-vars: 0 + react/sort-comp: + - 0 + - order: + - everything-else + - render diff --git a/codemods/sort-comp/tests/custom-sort-custom-sort/expected.tsx b/codemods/sort-comp/tests/custom-sort/expected.tsx similarity index 100% rename from codemods/sort-comp/tests/custom-sort-custom-sort/expected.tsx rename to codemods/sort-comp/tests/custom-sort/expected.tsx diff --git a/codemods/sort-comp/tests/custom-sort-custom-sort/input.tsx b/codemods/sort-comp/tests/custom-sort/input.tsx similarity index 100% rename from codemods/sort-comp/tests/custom-sort-custom-sort/input.tsx rename to codemods/sort-comp/tests/custom-sort/input.tsx diff --git a/codemods/sort-comp/tests/no-react-explicit-require/metrics.json b/codemods/sort-comp/tests/custom-sort/metrics.json similarity index 67% rename from codemods/sort-comp/tests/no-react-explicit-require/metrics.json rename to codemods/sort-comp/tests/custom-sort/metrics.json index a24fb9d..96517a4 100644 --- a/codemods/sort-comp/tests/no-react-explicit-require/metrics.json +++ b/codemods/sort-comp/tests/custom-sort/metrics.json @@ -2,7 +2,7 @@ "sort-comp-reorders": [ { "cardinality": { - "file": "tests/no-react-explicit-require/input.tsx", + "file": "tests/custom-sort/input.tsx", "kind": "createClass" }, "count": 1 diff --git a/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx b/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx deleted file mode 100644 index 7184b23..0000000 --- a/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var createClass = require('create-react-class'); - -var C = createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx b/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx deleted file mode 100644 index 7184b23..0000000 --- a/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var createClass = require('create-react-class'); - -var C = createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx b/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx deleted file mode 100644 index 152ec11..0000000 --- a/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var C = React.createClass({ - a() {}, - - render() { - return
    ; - }, -}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/input.tsx b/codemods/sort-comp/tests/no-react-explicit-require/input.tsx deleted file mode 100644 index d86a4e7..0000000 --- a/codemods/sort-comp/tests/no-react-explicit-require/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var C = React.createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/test.config.json b/codemods/sort-comp/tests/no-react-explicit-require/test.config.json deleted file mode 100644 index 4858435..0000000 --- a/codemods/sort-comp/tests/no-react-explicit-require/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "explicit-require": false - } -} diff --git a/codemods/sort-comp/tests/no-react-no-change/expected.tsx b/codemods/sort-comp/tests/no-react-no-change/expected.tsx deleted file mode 100644 index d86a4e7..0000000 --- a/codemods/sort-comp/tests/no-react-no-change/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var C = React.createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/no-react-no-change/input.tsx b/codemods/sort-comp/tests/no-react-no-change/input.tsx deleted file mode 100644 index d86a4e7..0000000 --- a/codemods/sort-comp/tests/no-react-no-change/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var C = React.createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx b/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx deleted file mode 100644 index 97a64fb..0000000 --- a/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from 'other-lib'; - -class C extends Component { - render() { - return
    ; - } - - a() {} -} diff --git a/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx b/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx deleted file mode 100644 index 97a64fb..0000000 --- a/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from 'other-lib'; - -class C extends Component { - render() { - return
    ; - } - - a() {} -} From 2eaaa236aab6dd8498041050dab26038d9d77f3d Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 17:08:06 +0200 Subject: [PATCH 13/29] clean up tests --- codemods/find-dom-node/README.md | 2 - codemods/find-dom-node/package.json | 2 +- .../find-dom-node/scripts/parity-tests.mjs | 186 ------------------ .../tests/computed-ref-alias/expected.tsx | 8 + .../tests/computed-ref-alias/input.tsx | 8 + .../tests/computed-ref-alias/metrics.json | 10 + .../tests/deep-refs-chain/expected.tsx | 6 + .../tests/deep-refs-chain/input.tsx | 6 + .../tests/deep-refs-chain/metrics.json | 10 + .../tests/es6-class-no-change/expected.tsx | 7 + .../tests/es6-class-no-change/input.tsx | 7 + .../export-default-no-change/expected.tsx | 7 + .../tests/export-default-no-change/input.tsx | 7 + .../tests/helper-no-change/expected.tsx | 2 + .../tests/helper-no-change/input.tsx | 2 + .../tests/module-exports/expected.tsx | 7 + .../tests/module-exports/input.tsx | 7 + .../tests/module-exports/metrics.json | 10 + .../expected.tsx | 5 + .../input.tsx | 5 + .../metrics.json | 10 + .../test.config.json | 5 + .../tests/no-react-create-class/expected.tsx | 5 + .../tests/no-react-create-class/input.tsx | 5 + codemods/pure-render-mixin/README.md | 2 - codemods/pure-render-mixin/package.json | 2 +- .../scripts/parity-tests.mjs | 130 ------------ .../expected.tsx | 11 ++ .../input.tsx | 11 ++ .../no-react-explicit-require/expected.tsx | 10 + .../tests/no-react-explicit-require/input.tsx | 9 + .../no-react-explicit-require/metrics.json | 10 + .../test.config.json | 5 + .../tests/no-react-no-change/expected.tsx | 9 + .../tests/no-react-no-change/input.tsx | 9 + codemods/sort-comp/README.md | 4 +- codemods/sort-comp/package.json | 2 +- codemods/sort-comp/scripts/parity-tests.mjs | 173 ---------------- .../expected.tsx | 9 + .../input.tsx | 9 + .../tests/methods-order-option/expected.tsx | 11 ++ .../tests/methods-order-option/input.tsx | 11 ++ .../tests/methods-order-option/metrics.json | 11 ++ .../methods-order-option/test.config.json | 5 + .../no-react-explicit-require/expected.tsx | 7 + .../tests/no-react-explicit-require/input.tsx | 7 + .../no-react-explicit-require/metrics.json | 11 ++ .../test.config.json | 5 + .../tests/no-react-no-change/expected.tsx | 7 + .../tests/no-react-no-change/input.tsx | 7 + .../expected.tsx | 9 + .../unrelated-component-no-change/input.tsx | 9 + 52 files changed, 335 insertions(+), 499 deletions(-) delete mode 100644 codemods/find-dom-node/scripts/parity-tests.mjs create mode 100644 codemods/find-dom-node/tests/computed-ref-alias/expected.tsx create mode 100644 codemods/find-dom-node/tests/computed-ref-alias/input.tsx create mode 100644 codemods/find-dom-node/tests/computed-ref-alias/metrics.json create mode 100644 codemods/find-dom-node/tests/deep-refs-chain/expected.tsx create mode 100644 codemods/find-dom-node/tests/deep-refs-chain/input.tsx create mode 100644 codemods/find-dom-node/tests/deep-refs-chain/metrics.json create mode 100644 codemods/find-dom-node/tests/es6-class-no-change/expected.tsx create mode 100644 codemods/find-dom-node/tests/es6-class-no-change/input.tsx create mode 100644 codemods/find-dom-node/tests/export-default-no-change/expected.tsx create mode 100644 codemods/find-dom-node/tests/export-default-no-change/input.tsx create mode 100644 codemods/find-dom-node/tests/helper-no-change/expected.tsx create mode 100644 codemods/find-dom-node/tests/helper-no-change/input.tsx create mode 100644 codemods/find-dom-node/tests/module-exports/expected.tsx create mode 100644 codemods/find-dom-node/tests/module-exports/input.tsx create mode 100644 codemods/find-dom-node/tests/module-exports/metrics.json create mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx create mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx create mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json create mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json create mode 100644 codemods/find-dom-node/tests/no-react-create-class/expected.tsx create mode 100644 codemods/find-dom-node/tests/no-react-create-class/input.tsx delete mode 100644 codemods/pure-render-mixin/scripts/parity-tests.mjs create mode 100644 codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx create mode 100644 codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx create mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx create mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx create mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json create mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json create mode 100644 codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx create mode 100644 codemods/pure-render-mixin/tests/no-react-no-change/input.tsx delete mode 100644 codemods/sort-comp/scripts/parity-tests.mjs create mode 100644 codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx create mode 100644 codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx create mode 100644 codemods/sort-comp/tests/methods-order-option/expected.tsx create mode 100644 codemods/sort-comp/tests/methods-order-option/input.tsx create mode 100644 codemods/sort-comp/tests/methods-order-option/metrics.json create mode 100644 codemods/sort-comp/tests/methods-order-option/test.config.json create mode 100644 codemods/sort-comp/tests/no-react-explicit-require/expected.tsx create mode 100644 codemods/sort-comp/tests/no-react-explicit-require/input.tsx create mode 100644 codemods/sort-comp/tests/no-react-explicit-require/metrics.json create mode 100644 codemods/sort-comp/tests/no-react-explicit-require/test.config.json create mode 100644 codemods/sort-comp/tests/no-react-no-change/expected.tsx create mode 100644 codemods/sort-comp/tests/no-react-no-change/input.tsx create mode 100644 codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx create mode 100644 codemods/sort-comp/tests/unrelated-component-no-change/input.tsx diff --git a/codemods/find-dom-node/README.md b/codemods/find-dom-node/README.md index 3a59472..2ffc54d 100644 --- a/codemods/find-dom-node/README.md +++ b/codemods/find-dom-node/README.md @@ -18,5 +18,3 @@ codemod run @react/find-dom-node pnpm test pnpm check-types ``` - -The checked-in `tests/` directory preserves the exact legacy fixture surface. Additional rollout-safety parity cases are verified in `scripts/parity-tests.mjs`. diff --git a/codemods/find-dom-node/package.json b/codemods/find-dom-node/package.json index a082821..de25953 100644 --- a/codemods/find-dom-node/package.json +++ b/codemods/find-dom-node/package.json @@ -4,7 +4,7 @@ "description": "Replace getDOMNode() with React.findDOMNode()", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/parity-tests.mjs", + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/find-dom-node/scripts/parity-tests.mjs b/codemods/find-dom-node/scripts/parity-tests.mjs deleted file mode 100644 index d805a79..0000000 --- a/codemods/find-dom-node/scripts/parity-tests.mjs +++ /dev/null @@ -1,186 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __filename = fileURLToPath(import.meta.url); -const packageDir = path.dirname(path.dirname(__filename)); -const workflowPath = path.join(packageDir, "workflow.yaml"); - -function runJssg(source, params = {}) { - const dir = mkdtempSync(path.join(tmpdir(), "find-dom-node-")); - const inputPath = path.join(dir, "input.tsx"); - writeFileSync(inputPath, source, "utf8"); - writeFileSync(path.join(dir, ".gitignore"), "", "utf8"); - - try { - spawnSync("git", ["init", "-q"], { cwd: dir }); - spawnSync("git", ["add", "input.tsx", ".gitignore"], { cwd: dir }); - spawnSync("git", ["commit", "-qm", "init"], { cwd: dir }); - - const args = [ - "dlx", - "codemod@latest", - "workflow", - "run", - "-w", - workflowPath, - "--target", - dir, - "--allow-dirty", - ]; - for (const [key, value] of Object.entries(params)) { - args.push("--param", `${key}=${value}`); - } - - const result = spawnSync("pnpm", args, { - cwd: packageDir, - encoding: "utf8", - timeout: 30000, - }); - assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); - return readFileSync(inputPath, "utf8").trim(); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -} - -const parityCases = [ - { - name: "module.exports createClass", - input: `var React = require('React'); - -module.exports = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); -`, - expected: `var React = require('React'); - -module.exports = React.createClass({ - render: function() { - return React.findDOMNode(this); - }, -});`, - }, - { - name: "createClass without React stays unchanged by default", - input: `var Composer = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); -`, - expected: `var Composer = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -});`, - }, - { - name: "createClass without React transforms with explicit-require false", - input: `var Composer = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); -`, - params: { "explicit-require": false }, - expected: `var Composer = React.createClass({ - render: function() { - return React.findDOMNode(this); - }, -});`, - }, - { - name: "es6 classes stay unchanged", - input: `var React = require('React'); - -class C extends React.Component { - render() { - return this.getDOMNode(); - } -} -`, - expected: `var React = require('React'); - -class C extends React.Component { - render() { - return this.getDOMNode(); - } -}`, - }, - { - name: "non-React helpers stay unchanged", - input: `const helper = foo(); -helper.getDOMNode(); -`, - expected: `const helper = foo(); -helper.getDOMNode();`, - }, - { - name: "computed refs alias is transformed", - input: `var React = require('React'); -var C = React.createClass({ - render: function() { - var ref = 'foo'; - var thing = this.refs[ref]; - return thing.getDOMNode(); - } -}); -`, - expected: `var React = require('React'); -var C = React.createClass({ - render: function() { - var ref = 'foo'; - var thing = this.refs[ref]; - return React.findDOMNode(thing); - } -});`, - }, - { - name: "deep refs chain is transformed", - input: `var React = require('React'); -var C = React.createClass({ - render: function() { - return this.refs.main.refs.list.getDOMNode(); - } -}); -`, - expected: `var React = require('React'); -var C = React.createClass({ - render: function() { - return React.findDOMNode(this.refs.main.refs.list); - } -});`, - }, - { - name: "export default createClass remains unchanged", - input: `var React = require('React'); - -export default React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); -`, - expected: `var React = require('React'); - -export default React.createClass({ - render: function() { - return this.getDOMNode(); - }, -});`, - }, -]; - -for (const testCase of parityCases) { - test(`matches parity case: ${testCase.name}`, () => { - const output = runJssg(testCase.input, testCase.params ?? {}); - assert.strictEqual(output, testCase.expected); - }); -} diff --git a/codemods/find-dom-node/tests/computed-ref-alias/expected.tsx b/codemods/find-dom-node/tests/computed-ref-alias/expected.tsx new file mode 100644 index 0000000..b66b1ac --- /dev/null +++ b/codemods/find-dom-node/tests/computed-ref-alias/expected.tsx @@ -0,0 +1,8 @@ +var React = require('React'); +var C = React.createClass({ + render: function() { + var ref = 'foo'; + var thing = this.refs[ref]; + return React.findDOMNode(thing); + } +}); diff --git a/codemods/find-dom-node/tests/computed-ref-alias/input.tsx b/codemods/find-dom-node/tests/computed-ref-alias/input.tsx new file mode 100644 index 0000000..cd5129a --- /dev/null +++ b/codemods/find-dom-node/tests/computed-ref-alias/input.tsx @@ -0,0 +1,8 @@ +var React = require('React'); +var C = React.createClass({ + render: function() { + var ref = 'foo'; + var thing = this.refs[ref]; + return thing.getDOMNode(); + } +}); diff --git a/codemods/find-dom-node/tests/computed-ref-alias/metrics.json b/codemods/find-dom-node/tests/computed-ref-alias/metrics.json new file mode 100644 index 0000000..fe6a6e0 --- /dev/null +++ b/codemods/find-dom-node/tests/computed-ref-alias/metrics.json @@ -0,0 +1,10 @@ +{ + "find-dom-node-replacements": [ + { + "cardinality": { + "file": "tests/computed-ref-alias/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/deep-refs-chain/expected.tsx b/codemods/find-dom-node/tests/deep-refs-chain/expected.tsx new file mode 100644 index 0000000..dee893c --- /dev/null +++ b/codemods/find-dom-node/tests/deep-refs-chain/expected.tsx @@ -0,0 +1,6 @@ +var React = require('React'); +var C = React.createClass({ + render: function() { + return React.findDOMNode(this.refs.main.refs.list); + } +}); diff --git a/codemods/find-dom-node/tests/deep-refs-chain/input.tsx b/codemods/find-dom-node/tests/deep-refs-chain/input.tsx new file mode 100644 index 0000000..7f94962 --- /dev/null +++ b/codemods/find-dom-node/tests/deep-refs-chain/input.tsx @@ -0,0 +1,6 @@ +var React = require('React'); +var C = React.createClass({ + render: function() { + return this.refs.main.refs.list.getDOMNode(); + } +}); diff --git a/codemods/find-dom-node/tests/deep-refs-chain/metrics.json b/codemods/find-dom-node/tests/deep-refs-chain/metrics.json new file mode 100644 index 0000000..ae90bb8 --- /dev/null +++ b/codemods/find-dom-node/tests/deep-refs-chain/metrics.json @@ -0,0 +1,10 @@ +{ + "find-dom-node-replacements": [ + { + "cardinality": { + "file": "tests/deep-refs-chain/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx b/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx new file mode 100644 index 0000000..464aee7 --- /dev/null +++ b/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +class C extends React.Component { + render() { + return this.getDOMNode(); + } +} diff --git a/codemods/find-dom-node/tests/es6-class-no-change/input.tsx b/codemods/find-dom-node/tests/es6-class-no-change/input.tsx new file mode 100644 index 0000000..464aee7 --- /dev/null +++ b/codemods/find-dom-node/tests/es6-class-no-change/input.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +class C extends React.Component { + render() { + return this.getDOMNode(); + } +} diff --git a/codemods/find-dom-node/tests/export-default-no-change/expected.tsx b/codemods/find-dom-node/tests/export-default-no-change/expected.tsx new file mode 100644 index 0000000..97fa6eb --- /dev/null +++ b/codemods/find-dom-node/tests/export-default-no-change/expected.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +export default React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/find-dom-node/tests/export-default-no-change/input.tsx b/codemods/find-dom-node/tests/export-default-no-change/input.tsx new file mode 100644 index 0000000..97fa6eb --- /dev/null +++ b/codemods/find-dom-node/tests/export-default-no-change/input.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +export default React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/find-dom-node/tests/helper-no-change/expected.tsx b/codemods/find-dom-node/tests/helper-no-change/expected.tsx new file mode 100644 index 0000000..281d7fa --- /dev/null +++ b/codemods/find-dom-node/tests/helper-no-change/expected.tsx @@ -0,0 +1,2 @@ +const helper = foo(); +helper.getDOMNode(); diff --git a/codemods/find-dom-node/tests/helper-no-change/input.tsx b/codemods/find-dom-node/tests/helper-no-change/input.tsx new file mode 100644 index 0000000..281d7fa --- /dev/null +++ b/codemods/find-dom-node/tests/helper-no-change/input.tsx @@ -0,0 +1,2 @@ +const helper = foo(); +helper.getDOMNode(); diff --git a/codemods/find-dom-node/tests/module-exports/expected.tsx b/codemods/find-dom-node/tests/module-exports/expected.tsx new file mode 100644 index 0000000..cab9f4c --- /dev/null +++ b/codemods/find-dom-node/tests/module-exports/expected.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +module.exports = React.createClass({ + render: function() { + return React.findDOMNode(this); + }, +}); diff --git a/codemods/find-dom-node/tests/module-exports/input.tsx b/codemods/find-dom-node/tests/module-exports/input.tsx new file mode 100644 index 0000000..9658cf5 --- /dev/null +++ b/codemods/find-dom-node/tests/module-exports/input.tsx @@ -0,0 +1,7 @@ +var React = require('React'); + +module.exports = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/find-dom-node/tests/module-exports/metrics.json b/codemods/find-dom-node/tests/module-exports/metrics.json new file mode 100644 index 0000000..42a4df1 --- /dev/null +++ b/codemods/find-dom-node/tests/module-exports/metrics.json @@ -0,0 +1,10 @@ +{ + "find-dom-node-replacements": [ + { + "cardinality": { + "file": "tests/module-exports/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx new file mode 100644 index 0000000..dd2e29f --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx @@ -0,0 +1,5 @@ +var Composer = React.createClass({ + render: function() { + return React.findDOMNode(this); + }, +}); diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx new file mode 100644 index 0000000..8b8bbf5 --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx @@ -0,0 +1,5 @@ +var Composer = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json new file mode 100644 index 0000000..6b828be --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json @@ -0,0 +1,10 @@ +{ + "find-dom-node-replacements": [ + { + "cardinality": { + "file": "tests/no-react-create-class-explicit-require/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json new file mode 100644 index 0000000..4858435 --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "explicit-require": false + } +} diff --git a/codemods/find-dom-node/tests/no-react-create-class/expected.tsx b/codemods/find-dom-node/tests/no-react-create-class/expected.tsx new file mode 100644 index 0000000..8b8bbf5 --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class/expected.tsx @@ -0,0 +1,5 @@ +var Composer = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/find-dom-node/tests/no-react-create-class/input.tsx b/codemods/find-dom-node/tests/no-react-create-class/input.tsx new file mode 100644 index 0000000..8b8bbf5 --- /dev/null +++ b/codemods/find-dom-node/tests/no-react-create-class/input.tsx @@ -0,0 +1,5 @@ +var Composer = React.createClass({ + render: function() { + return this.getDOMNode(); + }, +}); diff --git a/codemods/pure-render-mixin/README.md b/codemods/pure-render-mixin/README.md index a40a0af..847c05c 100644 --- a/codemods/pure-render-mixin/README.md +++ b/codemods/pure-render-mixin/README.md @@ -19,5 +19,3 @@ codemod run @react/pure-render-mixin pnpm test pnpm check-types ``` - -The checked-in `tests/` directory preserves the exact legacy fixture surface. Additional rollout-safety parity cases are verified in `scripts/parity-tests.mjs`. diff --git a/codemods/pure-render-mixin/package.json b/codemods/pure-render-mixin/package.json index fa5f796..83daaf7 100644 --- a/codemods/pure-render-mixin/package.json +++ b/codemods/pure-render-mixin/package.json @@ -4,7 +4,7 @@ "description": "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/parity-tests.mjs", + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/pure-render-mixin/scripts/parity-tests.mjs b/codemods/pure-render-mixin/scripts/parity-tests.mjs deleted file mode 100644 index 07c73d2..0000000 --- a/codemods/pure-render-mixin/scripts/parity-tests.mjs +++ /dev/null @@ -1,130 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __filename = fileURLToPath(import.meta.url); -const packageDir = path.dirname(path.dirname(__filename)); -const workflowPath = path.join(packageDir, "workflow.yaml"); - -function runJssg(source, params = {}) { - const dir = mkdtempSync(path.join(tmpdir(), "pure-render-mixin-")); - const inputPath = path.join(dir, "input.tsx"); - writeFileSync(inputPath, source, "utf8"); - writeFileSync(path.join(dir, ".gitignore"), "", "utf8"); - - try { - spawnSync("git", ["init", "-q"], { cwd: dir }); - spawnSync("git", ["add", "input.tsx", ".gitignore"], { cwd: dir }); - spawnSync("git", ["commit", "-qm", "init"], { cwd: dir }); - - const args = [ - "dlx", - "codemod@latest", - "workflow", - "run", - "-w", - workflowPath, - "--target", - dir, - "--allow-dirty", - ]; - for (const [key, value] of Object.entries(params)) { - args.push("--param", `${key}=${value}`); - } - - const result = spawnSync("pnpm", args, { - cwd: packageDir, - encoding: "utf8", - timeout: 30000, - }); - assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); - return readFileSync(inputPath, "utf8").trim(); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -} - -const parityCases = [ - { - name: "createClass without React stays unchanged by default", - input: `var PureRenderMixin = React.addons.PureRenderMixin; - -var C = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); -`, - expected: `var PureRenderMixin = React.addons.PureRenderMixin; - -var C = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -});`, - }, - { - name: "createClass without React transforms with explicit-require false", - input: `var PureRenderMixin = React.addons.PureRenderMixin; - -var C = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); -`, - params: { "explicit-require": false }, - expected: `var C = React.createClass({ - shouldComponentUpdate: function(nextProps, nextState) { - return React.addons.shallowCompare(this, nextProps, nextState); - }, - - render: function() { - return
    ; - }, -});`, - }, - { - name: "identifier createClass stays unchanged", - input: `var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; -var createClass = require('create-react-class'); - -var C = createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); -`, - expected: `var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; -var createClass = require('create-react-class'); - -var C = createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -});`, - }, -]; - -for (const testCase of parityCases) { - test(`matches parity case: ${testCase.name}`, () => { - const output = runJssg(testCase.input, testCase.params ?? {}); - assert.strictEqual(output, testCase.expected); - }); -} diff --git a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx new file mode 100644 index 0000000..bd81dcc --- /dev/null +++ b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx @@ -0,0 +1,11 @@ +var React = require('react/addons'); +var PureRenderMixin = React.addons.PureRenderMixin; +var createClass = require('create-react-class'); + +var C = createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx new file mode 100644 index 0000000..bd81dcc --- /dev/null +++ b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx @@ -0,0 +1,11 @@ +var React = require('react/addons'); +var PureRenderMixin = React.addons.PureRenderMixin; +var createClass = require('create-react-class'); + +var C = createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx b/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx new file mode 100644 index 0000000..47bd5ed --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx @@ -0,0 +1,10 @@ + +var C = React.createClass({ + shouldComponentUpdate: function(nextProps, nextState) { + return React.addons.shallowCompare(this, nextProps, nextState); + }, + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx b/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx new file mode 100644 index 0000000..13cd748 --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx @@ -0,0 +1,9 @@ +var PureRenderMixin = React.addons.PureRenderMixin; + +var C = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json b/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json new file mode 100644 index 0000000..fd0f9ae --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json @@ -0,0 +1,10 @@ +{ + "pure-render-mixin-replacements": [ + { + "cardinality": { + "file": "tests/no-react-explicit-require/input.tsx" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json b/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json new file mode 100644 index 0000000..4858435 --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "explicit-require": false + } +} diff --git a/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx b/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx new file mode 100644 index 0000000..13cd748 --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx @@ -0,0 +1,9 @@ +var PureRenderMixin = React.addons.PureRenderMixin; + +var C = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx b/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx new file mode 100644 index 0000000..13cd748 --- /dev/null +++ b/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx @@ -0,0 +1,9 @@ +var PureRenderMixin = React.addons.PureRenderMixin; + +var C = React.createClass({ + mixins: [PureRenderMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/sort-comp/README.md b/codemods/sort-comp/README.md index 04b52c8..0b73b11 100644 --- a/codemods/sort-comp/README.md +++ b/codemods/sort-comp/README.md @@ -20,6 +20,4 @@ pnpm test pnpm check-types ``` -The checked-in `tests/` directory preserves the exact legacy fixture surface. Additional rollout-safety parity cases are verified in `scripts/parity-tests.mjs`. - -The remaining blocker is full parity for shareable / fully resolved ESLint config resolution. The legacy codemod delegated that to ESLint’s full config machinery. Inside JSSG, I implemented a clean classic-config reader that covers the legacy fixture cases and direct local config branches, but I did not ship a heavier config-execution workaround for every ESLint extends shape. \ No newline at end of file +The remaining blocker is full parity for shareable / fully resolved ESLint config resolution. The legacy codemod delegated that to ESLint’s full config machinery. Inside JSSG, I implemented a clean classic-config reader that covers the legacy fixture cases and direct local config branches, but I did not ship a heavier config-execution workaround for every ESLint extends shape. diff --git a/codemods/sort-comp/package.json b/codemods/sort-comp/package.json index c4edffd..b1a4acf 100644 --- a/codemods/sort-comp/package.json +++ b/codemods/sort-comp/package.json @@ -4,7 +4,7 @@ "description": "Reorder React component methods to match eslint-plugin-react sort-comp rule", "type": "module", "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/parity-tests.mjs", + "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", "check-types": "tsc --noEmit" }, "devDependencies": { diff --git a/codemods/sort-comp/scripts/parity-tests.mjs b/codemods/sort-comp/scripts/parity-tests.mjs deleted file mode 100644 index c8326a8..0000000 --- a/codemods/sort-comp/scripts/parity-tests.mjs +++ /dev/null @@ -1,173 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __filename = fileURLToPath(import.meta.url); -const packageDir = path.dirname(path.dirname(__filename)); -const codemodFile = path.join(packageDir, "scripts/codemod.ts"); - -function writeExtraFiles(root, files) { - for (const file of files) { - const fullPath = path.join(root, file.name); - mkdirSync(path.dirname(fullPath), { recursive: true }); - writeFileSync(fullPath, file.content, "utf8"); - } -} - -function runJssgWithFiles(source, params = {}, extraFiles = []) { - const root = mkdtempSync(path.join(tmpdir(), "sort-comp-")); - const caseDir = path.join(root, "case"); - mkdirSync(caseDir); - const inputPath = path.join(caseDir, "input.tsx"); - writeFileSync(inputPath, source, "utf8"); - writeFileSync(path.join(caseDir, "expected.tsx"), source, "utf8"); - writeExtraFiles(root, extraFiles); - - try { - if (Object.keys(params).length > 0) { - writeFileSync(path.join(caseDir, "test.config.json"), JSON.stringify({ params }, null, 2)); - } - const result = spawnSync("pnpm", [ - "dlx", - "codemod@latest", - "jssg", - "test", - "-l", - "tsx", - codemodFile, - root, - "-u", - ], { - cwd: packageDir, - encoding: "utf8", - timeout: 30000, - }); - assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); - return readFileSync(path.join(caseDir, "expected.tsx"), "utf8").trim(); - } finally { - rmSync(root, { recursive: true, force: true }); - } -} - -const parityCases = [ - { - name: "createClass without React stays unchanged by default", - input: `var C = React.createClass({ - render() { - return
    ; - }, - - a() {}, -}); -`, - expected: `var C = React.createClass({ - render() { - return
    ; - }, - - a() {}, -});`, - }, - { - name: "createClass without React sorts with explicit-require false", - input: `var C = React.createClass({ - render() { - return
    ; - }, - - a() {}, -}); -`, - params: { "explicit-require": false }, - expected: `var C = React.createClass({ - a() {}, - - render() { - return
    ; - }, -});`, - }, - { - name: "identifier createClass stays unchanged", - input: `var createClass = require('create-react-class'); - -var C = createClass({ - render() { - return
    ; - }, - - a() {}, -}); -`, - expected: `var createClass = require('create-react-class'); - -var C = createClass({ - render() { - return
    ; - }, - - a() {}, -});`, - }, - { - name: "unrelated component stays unchanged", - input: `import { Component } from 'other-lib'; - -class C extends Component { - render() { - return
    ; - } - - a() {} -} -`, - expected: `import { Component } from 'other-lib'; - -class C extends Component { - render() { - return
    ; - } - - a() {} -}`, - }, - { - name: "methodsOrder option overrides built-in order", - input: `var React = require('react/addons'); - -var C = React.createClass({ - render() { - return
    ; - }, - - z() {}, - - componentDidMount() {}, -}); -`, - params: { methodsOrder: JSON.stringify(["componentDidMount", "everything-else", "render"]) }, - expected: `var React = require('react/addons'); - -var C = React.createClass({ - componentDidMount() {}, - - z() {}, - - render() { - return
    ; - }, -});`, - }, -]; - -for (const testCase of parityCases) { - test(`matches parity case: ${testCase.name}`, () => { - const params = { ...(testCase.params ?? {}) }; - const output = runJssgWithFiles(testCase.input, params, testCase.extraFiles ?? []); - assert.strictEqual(output, testCase.expected); - }); -} diff --git a/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx b/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx new file mode 100644 index 0000000..7184b23 --- /dev/null +++ b/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx @@ -0,0 +1,9 @@ +var createClass = require('create-react-class'); + +var C = createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx b/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx new file mode 100644 index 0000000..7184b23 --- /dev/null +++ b/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx @@ -0,0 +1,9 @@ +var createClass = require('create-react-class'); + +var C = createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/methods-order-option/expected.tsx b/codemods/sort-comp/tests/methods-order-option/expected.tsx new file mode 100644 index 0000000..658d96c --- /dev/null +++ b/codemods/sort-comp/tests/methods-order-option/expected.tsx @@ -0,0 +1,11 @@ +var React = require('react/addons'); + +var C = React.createClass({ + componentDidMount() {}, + + z() {}, + + render() { + return
    ; + }, +}); diff --git a/codemods/sort-comp/tests/methods-order-option/input.tsx b/codemods/sort-comp/tests/methods-order-option/input.tsx new file mode 100644 index 0000000..81a901c --- /dev/null +++ b/codemods/sort-comp/tests/methods-order-option/input.tsx @@ -0,0 +1,11 @@ +var React = require('react/addons'); + +var C = React.createClass({ + render() { + return
    ; + }, + + z() {}, + + componentDidMount() {}, +}); diff --git a/codemods/sort-comp/tests/methods-order-option/metrics.json b/codemods/sort-comp/tests/methods-order-option/metrics.json new file mode 100644 index 0000000..1723811 --- /dev/null +++ b/codemods/sort-comp/tests/methods-order-option/metrics.json @@ -0,0 +1,11 @@ +{ + "sort-comp-reorders": [ + { + "cardinality": { + "file": "tests/methods-order-option/input.tsx", + "kind": "createClass" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/sort-comp/tests/methods-order-option/test.config.json b/codemods/sort-comp/tests/methods-order-option/test.config.json new file mode 100644 index 0000000..f70c53c --- /dev/null +++ b/codemods/sort-comp/tests/methods-order-option/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "methodsOrder": ["componentDidMount", "everything-else", "render"] + } +} diff --git a/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx b/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx new file mode 100644 index 0000000..152ec11 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx @@ -0,0 +1,7 @@ +var C = React.createClass({ + a() {}, + + render() { + return
    ; + }, +}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/input.tsx b/codemods/sort-comp/tests/no-react-explicit-require/input.tsx new file mode 100644 index 0000000..d86a4e7 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-explicit-require/input.tsx @@ -0,0 +1,7 @@ +var C = React.createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/metrics.json b/codemods/sort-comp/tests/no-react-explicit-require/metrics.json new file mode 100644 index 0000000..a24fb9d --- /dev/null +++ b/codemods/sort-comp/tests/no-react-explicit-require/metrics.json @@ -0,0 +1,11 @@ +{ + "sort-comp-reorders": [ + { + "cardinality": { + "file": "tests/no-react-explicit-require/input.tsx", + "kind": "createClass" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/sort-comp/tests/no-react-explicit-require/test.config.json b/codemods/sort-comp/tests/no-react-explicit-require/test.config.json new file mode 100644 index 0000000..4858435 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-explicit-require/test.config.json @@ -0,0 +1,5 @@ +{ + "params": { + "explicit-require": false + } +} diff --git a/codemods/sort-comp/tests/no-react-no-change/expected.tsx b/codemods/sort-comp/tests/no-react-no-change/expected.tsx new file mode 100644 index 0000000..d86a4e7 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-no-change/expected.tsx @@ -0,0 +1,7 @@ +var C = React.createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/no-react-no-change/input.tsx b/codemods/sort-comp/tests/no-react-no-change/input.tsx new file mode 100644 index 0000000..d86a4e7 --- /dev/null +++ b/codemods/sort-comp/tests/no-react-no-change/input.tsx @@ -0,0 +1,7 @@ +var C = React.createClass({ + render() { + return
    ; + }, + + a() {}, +}); diff --git a/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx b/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx new file mode 100644 index 0000000..97a64fb --- /dev/null +++ b/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx @@ -0,0 +1,9 @@ +import { Component } from 'other-lib'; + +class C extends Component { + render() { + return
    ; + } + + a() {} +} diff --git a/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx b/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx new file mode 100644 index 0000000..97a64fb --- /dev/null +++ b/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx @@ -0,0 +1,9 @@ +import { Component } from 'other-lib'; + +class C extends Component { + render() { + return
    ; + } + + a() {} +} From 22cc0aec6850a80db5d070437168cf8efdcad8ba Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 17:08:15 +0200 Subject: [PATCH 14/29] fix use-form-state --- .../replace-use-form-state/scripts/codemod.ts | 90 +++++++++++-------- .../tests/aliased-to-same-name/expected.tsx | 2 + .../tests/aliased-to-same-name/input.tsx | 2 + .../tests/aliased-to-same-name/metrics.json | 11 +++ .../tests/default-and-named/expected.tsx | 2 + .../tests/default-and-named/input.tsx | 2 + .../tests/default-and-named/metrics.json | 18 ++++ .../expected.tsx | 2 + .../input.tsx | 2 + .../expected.tsx | 3 + .../input.tsx | 3 + .../tests/namespace-and-named/expected.tsx | 3 + .../tests/namespace-and-named/input.tsx | 3 + .../tests/namespace-and-named/metrics.json | 11 +++ .../tests/shadowed-local-binding/expected.tsx | 2 + .../tests/shadowed-local-binding/input.tsx | 2 + .../tests/shadowed-local-binding/metrics.json | 11 +++ .../tests/type-identifier-rename/expected.tsx | 3 + .../tests/type-identifier-rename/input.tsx | 3 + .../tests/type-identifier-rename/metrics.json | 11 +++ 20 files changed, 147 insertions(+), 39 deletions(-) create mode 100644 codemods/replace-use-form-state/tests/aliased-to-same-name/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/aliased-to-same-name/input.tsx create mode 100644 codemods/replace-use-form-state/tests/aliased-to-same-name/metrics.json create mode 100644 codemods/replace-use-form-state/tests/default-and-named/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/default-and-named/input.tsx create mode 100644 codemods/replace-use-form-state/tests/default-and-named/metrics.json create mode 100644 codemods/replace-use-form-state/tests/member-access-non-import-no-change/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/member-access-non-import-no-change/input.tsx create mode 100644 codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/input.tsx create mode 100644 codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/namespace-and-named/input.tsx create mode 100644 codemods/replace-use-form-state/tests/namespace-and-named/metrics.json create mode 100644 codemods/replace-use-form-state/tests/shadowed-local-binding/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/shadowed-local-binding/input.tsx create mode 100644 codemods/replace-use-form-state/tests/shadowed-local-binding/metrics.json create mode 100644 codemods/replace-use-form-state/tests/type-identifier-rename/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/type-identifier-rename/input.tsx create mode 100644 codemods/replace-use-form-state/tests/type-identifier-rename/metrics.json diff --git a/codemods/replace-use-form-state/scripts/codemod.ts b/codemods/replace-use-form-state/scripts/codemod.ts index e499a6a..8425623 100644 --- a/codemods/replace-use-form-state/scripts/codemod.ts +++ b/codemods/replace-use-form-state/scripts/codemod.ts @@ -13,6 +13,15 @@ const REACT_DOM_MODULE = "react-dom"; type ReactDOMImport = { name: string; type: "default" | "namespace" }; +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + if (!source) return null; + const fragment = source.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = source.text(); + return text.length >= 2 ? text.slice(1, -1) : null; +} + function getReactDOMImport(rootNode: SgNode): ReactDOMImport | null { const defaultImp = getImport(rootNode, { type: "default", from: REACT_DOM_MODULE }); if (defaultImp) return { name: defaultImp.alias, type: "default" }; @@ -39,17 +48,31 @@ function getReactDOMImport(rootNode: SgNode): ReactDOMImport | n return null; } +function firstReactDOMImport(rootNode: SgNode): SgNode | null { + return rootNode.findAll({ rule: { kind: "import_statement" } }) + .find((node) => importSource(node) === REACT_DOM_MODULE) ?? null; +} + +function matchingUseFormStateSpecifier(importNode: SgNode): SgNode | null { + return importNode.findAll({ + rule: { + kind: "import_specifier", + }, + }).find((spec) => spec.field("name")?.text() === "useFormState") ?? null; +} + +function replacementImportSpecifierText(usedName: string): string { + return usedName === "useFormState" + ? "useActionState" + : `useActionState as ${usedName}`; +} + const transform: Transform = async (root) => { const rootNode = root.root(); const edits: Edit[] = []; const metric = useMetricAtom("replace-use-form-state-migrations"); const reactDOMImport = getReactDOMImport(rootNode); - const useFormStateNamedImport = getImport(rootNode, { - type: "named", - name: "useFormState", - from: REACT_DOM_MODULE, - }); if (reactDOMImport) { const memberCalls = rootNode.findAll({ @@ -74,44 +97,33 @@ const transform: Transform = async (root) => { } } - if (useFormStateNamedImport && !useFormStateNamedImport.isNamespace) { - const importNode = useFormStateNamedImport.node.ancestors().find( - (a) => a.kind() === "import_statement" - ); - if (importNode) { - const specifiers = importNode.findAll({ - rule: { - kind: "import_specifier", - has: { - kind: "identifier", - regex: "^useFormState$", - }, - }, - }); - for (const spec of specifiers) { - const nameNode = spec.field("name"); - if (nameNode && nameNode.text() === "useFormState") { - edits.push(nameNode.replace("useActionState")); - } - } - } + const importNode = firstReactDOMImport(rootNode); + const specifier = importNode ? matchingUseFormStateSpecifier(importNode) : null; + if (!specifier) { + if (edits.length === 0) return null; + return rootNode.commitEdits(edits); + } - const localName = useFormStateNamedImport.alias; - if (localName === "useFormState") { - const refs = useFormStateNamedImport.node.references(); - for (const fileRef of refs) { - if (fileRef.root.filename() !== root.filename()) continue; - for (const node of fileRef.nodes) { - const inImport = node.ancestors().some((a) => a.kind() === "import_statement"); - if (!inImport) { - edits.push(node.replace("useActionState")); - } - } - } + const localName = specifier.field("alias")?.text() ?? specifier.field("name")?.text() ?? "useFormState"; + edits.push(specifier.replace(replacementImportSpecifierText(localName))); + + if (localName === "useFormState") { + const renameTargets = rootNode.findAll({ + rule: { + any: [ + { kind: "identifier", regex: "^useFormState$" }, + { kind: "type_identifier", regex: "^useFormState$" }, + ], + }, + }); + + for (const node of renameTargets) { + if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) continue; + edits.push(node.replace("useActionState")); } - metric.increment({ file: metricFile(root.filename()), pattern: "named-import" }); } + metric.increment({ file: metricFile(root.filename()), pattern: "named-import" }); if (edits.length === 0) return null; return rootNode.commitEdits(edits); }; diff --git a/codemods/replace-use-form-state/tests/aliased-to-same-name/expected.tsx b/codemods/replace-use-form-state/tests/aliased-to-same-name/expected.tsx new file mode 100644 index 0000000..98785a6 --- /dev/null +++ b/codemods/replace-use-form-state/tests/aliased-to-same-name/expected.tsx @@ -0,0 +1,2 @@ +import { useActionState } from 'react-dom'; +function C(){ return useActionState(a,0); } diff --git a/codemods/replace-use-form-state/tests/aliased-to-same-name/input.tsx b/codemods/replace-use-form-state/tests/aliased-to-same-name/input.tsx new file mode 100644 index 0000000..1701827 --- /dev/null +++ b/codemods/replace-use-form-state/tests/aliased-to-same-name/input.tsx @@ -0,0 +1,2 @@ +import { useFormState as useFormState } from 'react-dom'; +function C(){ return useFormState(a,0); } diff --git a/codemods/replace-use-form-state/tests/aliased-to-same-name/metrics.json b/codemods/replace-use-form-state/tests/aliased-to-same-name/metrics.json new file mode 100644 index 0000000..d06ff7d --- /dev/null +++ b/codemods/replace-use-form-state/tests/aliased-to-same-name/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-use-form-state-migrations": [ + { + "cardinality": { + "file": "tests/aliased-to-same-name/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-use-form-state/tests/default-and-named/expected.tsx b/codemods/replace-use-form-state/tests/default-and-named/expected.tsx new file mode 100644 index 0000000..a37fafb --- /dev/null +++ b/codemods/replace-use-form-state/tests/default-and-named/expected.tsx @@ -0,0 +1,2 @@ +import ReactDOM, { useActionState } from 'react-dom'; +function C(){ const x = useActionState(a,0); return ReactDOM.useActionState(b,1); } diff --git a/codemods/replace-use-form-state/tests/default-and-named/input.tsx b/codemods/replace-use-form-state/tests/default-and-named/input.tsx new file mode 100644 index 0000000..b1247ed --- /dev/null +++ b/codemods/replace-use-form-state/tests/default-and-named/input.tsx @@ -0,0 +1,2 @@ +import ReactDOM, { useFormState } from 'react-dom'; +function C(){ const x = useFormState(a,0); return ReactDOM.useFormState(b,1); } diff --git a/codemods/replace-use-form-state/tests/default-and-named/metrics.json b/codemods/replace-use-form-state/tests/default-and-named/metrics.json new file mode 100644 index 0000000..fc8f667 --- /dev/null +++ b/codemods/replace-use-form-state/tests/default-and-named/metrics.json @@ -0,0 +1,18 @@ +{ + "replace-use-form-state-migrations": [ + { + "cardinality": { + "file": "tests/default-and-named/input.tsx", + "pattern": "member-call" + }, + "count": 1 + }, + { + "cardinality": { + "file": "tests/default-and-named/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-use-form-state/tests/member-access-non-import-no-change/expected.tsx b/codemods/replace-use-form-state/tests/member-access-non-import-no-change/expected.tsx new file mode 100644 index 0000000..30ae5b3 --- /dev/null +++ b/codemods/replace-use-form-state/tests/member-access-non-import-no-change/expected.tsx @@ -0,0 +1,2 @@ +const ReactDOM = other; +function C(){ return ReactDOM.useFormState(a,0); } diff --git a/codemods/replace-use-form-state/tests/member-access-non-import-no-change/input.tsx b/codemods/replace-use-form-state/tests/member-access-non-import-no-change/input.tsx new file mode 100644 index 0000000..30ae5b3 --- /dev/null +++ b/codemods/replace-use-form-state/tests/member-access-non-import-no-change/input.tsx @@ -0,0 +1,2 @@ +const ReactDOM = other; +function C(){ return ReactDOM.useFormState(a,0); } diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx new file mode 100644 index 0000000..c3611b0 --- /dev/null +++ b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx @@ -0,0 +1,3 @@ +import { createPortal } from 'react-dom'; +import { useFormState } from 'react-dom'; +function C(){ return useFormState(a,0); } diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/input.tsx b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/input.tsx new file mode 100644 index 0000000..c3611b0 --- /dev/null +++ b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/input.tsx @@ -0,0 +1,3 @@ +import { createPortal } from 'react-dom'; +import { useFormState } from 'react-dom'; +function C(){ return useFormState(a,0); } diff --git a/codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx b/codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx new file mode 100644 index 0000000..517c6e5 --- /dev/null +++ b/codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx @@ -0,0 +1,3 @@ +import * as ReactDOM from 'react-dom'; +import { useFormState as useFormStateAlias } from 'react-dom'; +function C(){ const x = useFormStateAlias(a,0); return ReactDOM.useActionState(b,1); } diff --git a/codemods/replace-use-form-state/tests/namespace-and-named/input.tsx b/codemods/replace-use-form-state/tests/namespace-and-named/input.tsx new file mode 100644 index 0000000..537efc5 --- /dev/null +++ b/codemods/replace-use-form-state/tests/namespace-and-named/input.tsx @@ -0,0 +1,3 @@ +import * as ReactDOM from 'react-dom'; +import { useFormState as useFormStateAlias } from 'react-dom'; +function C(){ const x = useFormStateAlias(a,0); return ReactDOM.useFormState(b,1); } diff --git a/codemods/replace-use-form-state/tests/namespace-and-named/metrics.json b/codemods/replace-use-form-state/tests/namespace-and-named/metrics.json new file mode 100644 index 0000000..f922361 --- /dev/null +++ b/codemods/replace-use-form-state/tests/namespace-and-named/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-use-form-state-migrations": [ + { + "cardinality": { + "file": "tests/namespace-and-named/input.tsx", + "pattern": "member-call" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-use-form-state/tests/shadowed-local-binding/expected.tsx b/codemods/replace-use-form-state/tests/shadowed-local-binding/expected.tsx new file mode 100644 index 0000000..ab90583 --- /dev/null +++ b/codemods/replace-use-form-state/tests/shadowed-local-binding/expected.tsx @@ -0,0 +1,2 @@ +import { useActionState } from 'react-dom'; +function C(){ const useActionState = make(); return useActionState(a,0); } diff --git a/codemods/replace-use-form-state/tests/shadowed-local-binding/input.tsx b/codemods/replace-use-form-state/tests/shadowed-local-binding/input.tsx new file mode 100644 index 0000000..98f5afe --- /dev/null +++ b/codemods/replace-use-form-state/tests/shadowed-local-binding/input.tsx @@ -0,0 +1,2 @@ +import { useFormState } from 'react-dom'; +function C(){ const useFormState = make(); return useFormState(a,0); } diff --git a/codemods/replace-use-form-state/tests/shadowed-local-binding/metrics.json b/codemods/replace-use-form-state/tests/shadowed-local-binding/metrics.json new file mode 100644 index 0000000..54c7b7e --- /dev/null +++ b/codemods/replace-use-form-state/tests/shadowed-local-binding/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-use-form-state-migrations": [ + { + "cardinality": { + "file": "tests/shadowed-local-binding/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-use-form-state/tests/type-identifier-rename/expected.tsx b/codemods/replace-use-form-state/tests/type-identifier-rename/expected.tsx new file mode 100644 index 0000000..5516ef0 --- /dev/null +++ b/codemods/replace-use-form-state/tests/type-identifier-rename/expected.tsx @@ -0,0 +1,3 @@ +import { useActionState } from 'react-dom'; +type useActionState = number; +function C(){ return useActionState(a,0); } diff --git a/codemods/replace-use-form-state/tests/type-identifier-rename/input.tsx b/codemods/replace-use-form-state/tests/type-identifier-rename/input.tsx new file mode 100644 index 0000000..1724eee --- /dev/null +++ b/codemods/replace-use-form-state/tests/type-identifier-rename/input.tsx @@ -0,0 +1,3 @@ +import { useFormState } from 'react-dom'; +type useFormState = number; +function C(){ return useFormState(a,0); } diff --git a/codemods/replace-use-form-state/tests/type-identifier-rename/metrics.json b/codemods/replace-use-form-state/tests/type-identifier-rename/metrics.json new file mode 100644 index 0000000..adca29c --- /dev/null +++ b/codemods/replace-use-form-state/tests/type-identifier-rename/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-use-form-state-migrations": [ + { + "cardinality": { + "file": "tests/type-identifier-rename/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file From f0d54da5a183f9f8b57d99802dd7d6152c12a967 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 17:23:28 +0200 Subject: [PATCH 15/29] revert replace-act-import "improvement" --- codemods/replace-act-import/scripts/codemod.ts | 15 +-------------- .../tests/named-reexport-no-change/expected.tsx | 1 + .../tests/named-reexport-no-change/input.tsx | 1 + 3 files changed, 3 insertions(+), 14 deletions(-) create mode 100644 codemods/replace-act-import/tests/named-reexport-no-change/expected.tsx create mode 100644 codemods/replace-act-import/tests/named-reexport-no-change/input.tsx diff --git a/codemods/replace-act-import/scripts/codemod.ts b/codemods/replace-act-import/scripts/codemod.ts index b9003c2..bbaee28 100644 --- a/codemods/replace-act-import/scripts/codemod.ts +++ b/codemods/replace-act-import/scripts/codemod.ts @@ -162,20 +162,7 @@ const transform: Transform = async (root) => { const hasExportAll = exp.has({ rule: { kind: "namespace_export" } }) || exp.children().some((c) => c.text() === "*"); - const hasNamedAct = exp.find({ - rule: { - kind: "export_specifier", - has: { kind: "identifier", regex: "^act$" }, - }, - }); - - if (hasNamedAct) { - const stringNode = exp.find({ rule: { kind: "string", regex: "react-dom/test-utils" } }); - if (stringNode) { - edits.push(stringNode.replace('"react"')); - metric.increment({ file: metricFile(root.filename()), pattern: "re-export" }); - } - } else if (hasExportAll) { + if (hasExportAll) { const insertPos = exp.range().end.index; edits.push({ startPos: insertPos, diff --git a/codemods/replace-act-import/tests/named-reexport-no-change/expected.tsx b/codemods/replace-act-import/tests/named-reexport-no-change/expected.tsx new file mode 100644 index 0000000..2430a1e --- /dev/null +++ b/codemods/replace-act-import/tests/named-reexport-no-change/expected.tsx @@ -0,0 +1 @@ +export { act } from "react-dom/test-utils"; diff --git a/codemods/replace-act-import/tests/named-reexport-no-change/input.tsx b/codemods/replace-act-import/tests/named-reexport-no-change/input.tsx new file mode 100644 index 0000000..2430a1e --- /dev/null +++ b/codemods/replace-act-import/tests/named-reexport-no-change/input.tsx @@ -0,0 +1 @@ +export { act } from "react-dom/test-utils"; From 24417e6059f47144dfbf4a89d0e81d6c86e51527 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 17:37:30 +0200 Subject: [PATCH 16/29] fix `replace-string-ref` --- .../replace-string-ref/scripts/codemod.ts | 42 ++++++++++++++----- .../tests/export-default-class/expected.tsx | 4 ++ .../tests/export-default-class/input.tsx | 2 + .../tests/export-default-class/metrics.json | 11 +++++ .../expected.tsx | 2 + .../input.tsx | 2 + .../tests/multiple-string-refs/expected.tsx | 6 +++ .../tests/multiple-string-refs/input.tsx | 2 + .../tests/multiple-string-refs/metrics.json | 11 +++++ .../tests/namespace-import/expected.tsx | 4 ++ .../tests/namespace-import/input.tsx | 2 + .../tests/namespace-import/metrics.json | 11 +++++ 12 files changed, 88 insertions(+), 11 deletions(-) create mode 100644 codemods/replace-string-ref/tests/export-default-class/expected.tsx create mode 100644 codemods/replace-string-ref/tests/export-default-class/input.tsx create mode 100644 codemods/replace-string-ref/tests/export-default-class/metrics.json create mode 100644 codemods/replace-string-ref/tests/member-expression-ref-value-no-change/expected.tsx create mode 100644 codemods/replace-string-ref/tests/member-expression-ref-value-no-change/input.tsx create mode 100644 codemods/replace-string-ref/tests/multiple-string-refs/expected.tsx create mode 100644 codemods/replace-string-ref/tests/multiple-string-refs/input.tsx create mode 100644 codemods/replace-string-ref/tests/multiple-string-refs/metrics.json create mode 100644 codemods/replace-string-ref/tests/namespace-import/expected.tsx create mode 100644 codemods/replace-string-ref/tests/namespace-import/input.tsx create mode 100644 codemods/replace-string-ref/tests/namespace-import/metrics.json diff --git a/codemods/replace-string-ref/scripts/codemod.ts b/codemods/replace-string-ref/scripts/codemod.ts index 828493e..a78cbba 100644 --- a/codemods/replace-string-ref/scripts/codemod.ts +++ b/codemods/replace-string-ref/scripts/codemod.ts @@ -55,6 +55,27 @@ function isInsideReactClassComponent( return false; } +function namedReturnExpression(returnStmt: SgNode): SgNode | null { + return returnStmt.children().find((child) => child.isNamed() && child.kind() !== "comment") ?? null; +} + +function isFragmentElement(node: SgNode): boolean { + if (node.kind() !== "jsx_element") return false; + const opening = node.children().find((child) => child.kind() === "jsx_opening_element"); + if (!opening) return false; + const tagName = opening.children().find((child) => + child.kind() === "identifier" || child.kind() === "property_identifier" + ); + return !tagName; +} + +function shouldWrapReturnExpression(node: SgNode | null): node is SgNode { + if (!node) return false; + if (node.kind() === "jsx_self_closing_element") return true; + if (node.kind() === "jsx_element") return !isFragmentElement(node); + return false; +} + const transform: Transform = async (root) => { const rootNode = root.root(); const edits: Edit[] = []; @@ -107,25 +128,24 @@ const transform: Transform = async (root) => { }}`; edits.push(refAttr.replace(callbackRef)); - const jsx = refAttr.ancestors().find((a) => - a.kind() === "jsx_self_closing_element" || a.kind() === "jsx_element" - ); const returnStmt = refAttr.ancestors().find((a) => a.kind() === "return_statement"); + const returnExpr = returnStmt ? namedReturnExpression(returnStmt) : null; if ( - jsx && + returnExpr && returnStmt && - jsx.parent()?.kind() !== "parenthesized_expression" && - !wrappedJsxStarts.has(jsx.range().start.index) + shouldWrapReturnExpression(returnExpr) && + returnExpr.parent()?.kind() !== "parenthesized_expression" && + !wrappedJsxStarts.has(returnExpr.range().start.index) ) { - wrappedJsxStarts.add(jsx.range().start.index); + wrappedJsxStarts.add(returnExpr.range().start.index); edits.push({ - startPos: jsx.range().start.index, - endPos: jsx.range().start.index, + startPos: returnExpr.range().start.index, + endPos: returnExpr.range().start.index, insertedText: "(", }); edits.push({ - startPos: jsx.range().end.index, - endPos: jsx.range().end.index, + startPos: returnExpr.range().end.index, + endPos: returnExpr.range().end.index, insertedText: ")", }); } diff --git a/codemods/replace-string-ref/tests/export-default-class/expected.tsx b/codemods/replace-string-ref/tests/export-default-class/expected.tsx new file mode 100644 index 0000000..3ee5887 --- /dev/null +++ b/codemods/replace-string-ref/tests/export-default-class/expected.tsx @@ -0,0 +1,4 @@ +import React from "react"; +export default class C extends React.Component { render() { return (
    { + this.refs.refName = ref; + }} />); } } diff --git a/codemods/replace-string-ref/tests/export-default-class/input.tsx b/codemods/replace-string-ref/tests/export-default-class/input.tsx new file mode 100644 index 0000000..bdc33e9 --- /dev/null +++ b/codemods/replace-string-ref/tests/export-default-class/input.tsx @@ -0,0 +1,2 @@ +import React from "react"; +export default class C extends React.Component { render() { return
    ; } } diff --git a/codemods/replace-string-ref/tests/export-default-class/metrics.json b/codemods/replace-string-ref/tests/export-default-class/metrics.json new file mode 100644 index 0000000..8998681 --- /dev/null +++ b/codemods/replace-string-ref/tests/export-default-class/metrics.json @@ -0,0 +1,11 @@ +{ + "string-ref-replacements": [ + { + "cardinality": { + "file": "tests/export-default-class/input.tsx", + "refs": "refName" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-string-ref/tests/member-expression-ref-value-no-change/expected.tsx b/codemods/replace-string-ref/tests/member-expression-ref-value-no-change/expected.tsx new file mode 100644 index 0000000..fee1ffe --- /dev/null +++ b/codemods/replace-string-ref/tests/member-expression-ref-value-no-change/expected.tsx @@ -0,0 +1,2 @@ +import React from "react"; +class C extends React.Component { render(){ return
    ; } } diff --git a/codemods/replace-string-ref/tests/member-expression-ref-value-no-change/input.tsx b/codemods/replace-string-ref/tests/member-expression-ref-value-no-change/input.tsx new file mode 100644 index 0000000..fee1ffe --- /dev/null +++ b/codemods/replace-string-ref/tests/member-expression-ref-value-no-change/input.tsx @@ -0,0 +1,2 @@ +import React from "react"; +class C extends React.Component { render(){ return
    ; } } diff --git a/codemods/replace-string-ref/tests/multiple-string-refs/expected.tsx b/codemods/replace-string-ref/tests/multiple-string-refs/expected.tsx new file mode 100644 index 0000000..68c052a --- /dev/null +++ b/codemods/replace-string-ref/tests/multiple-string-refs/expected.tsx @@ -0,0 +1,6 @@ +import React from "react"; +class C extends React.Component { render() { return <>
    { + this.refs.a = ref; + }} /> { + this.refs.b = ref; + }} />; } } diff --git a/codemods/replace-string-ref/tests/multiple-string-refs/input.tsx b/codemods/replace-string-ref/tests/multiple-string-refs/input.tsx new file mode 100644 index 0000000..16a4334 --- /dev/null +++ b/codemods/replace-string-ref/tests/multiple-string-refs/input.tsx @@ -0,0 +1,2 @@ +import React from "react"; +class C extends React.Component { render() { return <>
    ; } } diff --git a/codemods/replace-string-ref/tests/multiple-string-refs/metrics.json b/codemods/replace-string-ref/tests/multiple-string-refs/metrics.json new file mode 100644 index 0000000..d2db766 --- /dev/null +++ b/codemods/replace-string-ref/tests/multiple-string-refs/metrics.json @@ -0,0 +1,11 @@ +{ + "string-ref-replacements": [ + { + "cardinality": { + "file": "tests/multiple-string-refs/input.tsx", + "refs": "a,b" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-string-ref/tests/namespace-import/expected.tsx b/codemods/replace-string-ref/tests/namespace-import/expected.tsx new file mode 100644 index 0000000..69b90a7 --- /dev/null +++ b/codemods/replace-string-ref/tests/namespace-import/expected.tsx @@ -0,0 +1,4 @@ +import * as React1 from "react"; +class C extends React1.Component { render() { return (
    { + this.refs.refName = ref; + }} />); } } diff --git a/codemods/replace-string-ref/tests/namespace-import/input.tsx b/codemods/replace-string-ref/tests/namespace-import/input.tsx new file mode 100644 index 0000000..9bca446 --- /dev/null +++ b/codemods/replace-string-ref/tests/namespace-import/input.tsx @@ -0,0 +1,2 @@ +import * as React1 from "react"; +class C extends React1.Component { render() { return
    ; } } diff --git a/codemods/replace-string-ref/tests/namespace-import/metrics.json b/codemods/replace-string-ref/tests/namespace-import/metrics.json new file mode 100644 index 0000000..8034886 --- /dev/null +++ b/codemods/replace-string-ref/tests/namespace-import/metrics.json @@ -0,0 +1,11 @@ +{ + "string-ref-replacements": [ + { + "cardinality": { + "file": "tests/namespace-import/input.tsx", + "refs": "refName" + }, + "count": 1 + } + ] +} \ No newline at end of file From 4a9df82633560ef9467c435619a4282fd9d155d3 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 18:05:07 +0200 Subject: [PATCH 17/29] improve `use-context-hook` --- codemods/use-context-hook/scripts/codemod.ts | 48 ++++++++++++------- .../tests/aliased-use-context/expected.tsx | 2 + .../tests/aliased-use-context/input.tsx | 2 + .../tests/aliased-use-context/metrics.json | 11 +++++ .../tests/shadowed-local-binding/expected.tsx | 2 + .../tests/shadowed-local-binding/input.tsx | 2 + .../tests/shadowed-local-binding/metrics.json | 11 +++++ .../tests/type-identifier-rename/expected.tsx | 3 ++ .../tests/type-identifier-rename/input.tsx | 3 ++ .../tests/type-identifier-rename/metrics.json | 11 +++++ 10 files changed, 78 insertions(+), 17 deletions(-) create mode 100644 codemods/use-context-hook/tests/aliased-use-context/expected.tsx create mode 100644 codemods/use-context-hook/tests/aliased-use-context/input.tsx create mode 100644 codemods/use-context-hook/tests/aliased-use-context/metrics.json create mode 100644 codemods/use-context-hook/tests/shadowed-local-binding/expected.tsx create mode 100644 codemods/use-context-hook/tests/shadowed-local-binding/input.tsx create mode 100644 codemods/use-context-hook/tests/shadowed-local-binding/metrics.json create mode 100644 codemods/use-context-hook/tests/type-identifier-rename/expected.tsx create mode 100644 codemods/use-context-hook/tests/type-identifier-rename/input.tsx create mode 100644 codemods/use-context-hook/tests/type-identifier-rename/metrics.json diff --git a/codemods/use-context-hook/scripts/codemod.ts b/codemods/use-context-hook/scripts/codemod.ts index 04c6f20..eb3a2cb 100644 --- a/codemods/use-context-hook/scripts/codemod.ts +++ b/codemods/use-context-hook/scripts/codemod.ts @@ -1,7 +1,7 @@ -import type { Transform, Edit } from "codemod:ast-grep"; +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport, addImport, removeImport } from "@jssg/utils/javascript/imports"; +import { getImport } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -22,6 +22,19 @@ const transform: Transform = async (root) => { const useContextLocalName = useContextImport?.alias; let needsUseNamedImport = false; + + const shouldSkipLegacyIdentifierRename = (node: SgNode): boolean => { + const parent = node.parent(); + if (!parent) return false; + if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) return true; + if ( + parent.kind() === "member_expression" && + parent.field("property")?.id() === node.id() + ) { + return true; + } + return false; + }; const reactMemberCalls = rootNode.findAll({ rule: { @@ -84,22 +97,23 @@ const transform: Transform = async (root) => { const isAliased = useContextImport && useContextImport.alias !== "useContext"; if (!isAliased) { - for (const call of useContextCalls) { - const callee = call.find({ - rule: { - kind: "identifier", - regex: `^${useContextLocalName}$`, - }, - }); - - if (callee) { - edits.push(callee.replace("use")); - transformMetric.increment({ - pattern: "useContext", - file: metricFile(root.filename()) - }); - } + const renameTargets = rootNode.findAll({ + rule: { + any: [ + { kind: "identifier", regex: `^${useContextLocalName}$` }, + { kind: "type_identifier", regex: `^${useContextLocalName}$` }, + ], + }, + }); + + for (const node of renameTargets) { + if (shouldSkipLegacyIdentifierRename(node)) continue; + edits.push(node.replace("use")); } + transformMetric.increment({ + pattern: "useContext", + file: metricFile(root.filename()) + }, useContextCalls.length); } else { transformMetric.increment({ pattern: "useContext (aliased)", diff --git a/codemods/use-context-hook/tests/aliased-use-context/expected.tsx b/codemods/use-context-hook/tests/aliased-use-context/expected.tsx new file mode 100644 index 0000000..f5c66fd --- /dev/null +++ b/codemods/use-context-hook/tests/aliased-use-context/expected.tsx @@ -0,0 +1,2 @@ +import { use as uc } from "react"; +const x = uc(ThemeContext); diff --git a/codemods/use-context-hook/tests/aliased-use-context/input.tsx b/codemods/use-context-hook/tests/aliased-use-context/input.tsx new file mode 100644 index 0000000..2290c34 --- /dev/null +++ b/codemods/use-context-hook/tests/aliased-use-context/input.tsx @@ -0,0 +1,2 @@ +import { useContext as uc } from "react"; +const x = uc(ThemeContext); diff --git a/codemods/use-context-hook/tests/aliased-use-context/metrics.json b/codemods/use-context-hook/tests/aliased-use-context/metrics.json new file mode 100644 index 0000000..50ae81a --- /dev/null +++ b/codemods/use-context-hook/tests/aliased-use-context/metrics.json @@ -0,0 +1,11 @@ +{ + "use-context-transformations": [ + { + "cardinality": { + "file": "tests/aliased-use-context/input.tsx", + "pattern": "useContext (aliased)" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/use-context-hook/tests/shadowed-local-binding/expected.tsx b/codemods/use-context-hook/tests/shadowed-local-binding/expected.tsx new file mode 100644 index 0000000..254ae32 --- /dev/null +++ b/codemods/use-context-hook/tests/shadowed-local-binding/expected.tsx @@ -0,0 +1,2 @@ +import { use } from "react"; +function f(use){ return use(Ctx); } diff --git a/codemods/use-context-hook/tests/shadowed-local-binding/input.tsx b/codemods/use-context-hook/tests/shadowed-local-binding/input.tsx new file mode 100644 index 0000000..e010190 --- /dev/null +++ b/codemods/use-context-hook/tests/shadowed-local-binding/input.tsx @@ -0,0 +1,2 @@ +import { useContext } from "react"; +function f(useContext){ return useContext(Ctx); } diff --git a/codemods/use-context-hook/tests/shadowed-local-binding/metrics.json b/codemods/use-context-hook/tests/shadowed-local-binding/metrics.json new file mode 100644 index 0000000..b71459b --- /dev/null +++ b/codemods/use-context-hook/tests/shadowed-local-binding/metrics.json @@ -0,0 +1,11 @@ +{ + "use-context-transformations": [ + { + "cardinality": { + "file": "tests/shadowed-local-binding/input.tsx", + "pattern": "useContext" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/use-context-hook/tests/type-identifier-rename/expected.tsx b/codemods/use-context-hook/tests/type-identifier-rename/expected.tsx new file mode 100644 index 0000000..76b6176 --- /dev/null +++ b/codemods/use-context-hook/tests/type-identifier-rename/expected.tsx @@ -0,0 +1,3 @@ +import { use } from "react"; +type use = number; +const x = use(Ctx); diff --git a/codemods/use-context-hook/tests/type-identifier-rename/input.tsx b/codemods/use-context-hook/tests/type-identifier-rename/input.tsx new file mode 100644 index 0000000..0a7b35b --- /dev/null +++ b/codemods/use-context-hook/tests/type-identifier-rename/input.tsx @@ -0,0 +1,3 @@ +import { useContext } from "react"; +type useContext = number; +const x = useContext(Ctx); diff --git a/codemods/use-context-hook/tests/type-identifier-rename/metrics.json b/codemods/use-context-hook/tests/type-identifier-rename/metrics.json new file mode 100644 index 0000000..ef8bf7f --- /dev/null +++ b/codemods/use-context-hook/tests/type-identifier-rename/metrics.json @@ -0,0 +1,11 @@ +{ + "use-context-transformations": [ + { + "cardinality": { + "file": "tests/type-identifier-rename/input.tsx", + "pattern": "useContext" + }, + "count": 1 + } + ] +} \ No newline at end of file From 0c0278880b6482f6427404206adb0329fe94a858 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 18:05:15 +0200 Subject: [PATCH 18/29] improve replace-string-ref --- codemods/replace-string-ref/scripts/codemod.ts | 18 ++++++++++++++---- .../tests/non-identifier-ref-name/expected.tsx | 4 ++++ .../tests/non-identifier-ref-name/input.tsx | 2 ++ .../tests/non-identifier-ref-name/metrics.json | 11 +++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 codemods/replace-string-ref/tests/non-identifier-ref-name/expected.tsx create mode 100644 codemods/replace-string-ref/tests/non-identifier-ref-name/input.tsx create mode 100644 codemods/replace-string-ref/tests/non-identifier-ref-name/metrics.json diff --git a/codemods/replace-string-ref/scripts/codemod.ts b/codemods/replace-string-ref/scripts/codemod.ts index a78cbba..54c8b5e 100644 --- a/codemods/replace-string-ref/scripts/codemod.ts +++ b/codemods/replace-string-ref/scripts/codemod.ts @@ -55,6 +55,19 @@ function isInsideReactClassComponent( return false; } +function isValidIdentifierName(name: string): boolean { + return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name); +} + +function callbackRefText(refName: string): string { + const assignmentTarget = isValidIdentifierName(refName) + ? `this.refs.${refName}` + : `this.refs[${JSON.stringify(refName)}]`; + return `ref={(ref) => { + ${assignmentTarget} = ref; + }}`; +} + function namedReturnExpression(returnStmt: SgNode): SgNode | null { return returnStmt.children().find((child) => child.isNamed() && child.kind() !== "comment") ?? null; } @@ -122,10 +135,7 @@ const transform: Transform = async (root) => { if (!stringFragment) continue; const refName = stringFragment.text(); - - const callbackRef = `ref={(ref) => { - this.refs.${refName} = ref; - }}`; + const callbackRef = callbackRefText(refName); edits.push(refAttr.replace(callbackRef)); const returnStmt = refAttr.ancestors().find((a) => a.kind() === "return_statement"); diff --git a/codemods/replace-string-ref/tests/non-identifier-ref-name/expected.tsx b/codemods/replace-string-ref/tests/non-identifier-ref-name/expected.tsx new file mode 100644 index 0000000..e31278e --- /dev/null +++ b/codemods/replace-string-ref/tests/non-identifier-ref-name/expected.tsx @@ -0,0 +1,4 @@ +import React from "react"; +class C extends React.Component { render(){ return (
    { + this.refs["foo-bar"] = ref; + }} />); } } diff --git a/codemods/replace-string-ref/tests/non-identifier-ref-name/input.tsx b/codemods/replace-string-ref/tests/non-identifier-ref-name/input.tsx new file mode 100644 index 0000000..218c61c --- /dev/null +++ b/codemods/replace-string-ref/tests/non-identifier-ref-name/input.tsx @@ -0,0 +1,2 @@ +import React from "react"; +class C extends React.Component { render(){ return
    ; } } diff --git a/codemods/replace-string-ref/tests/non-identifier-ref-name/metrics.json b/codemods/replace-string-ref/tests/non-identifier-ref-name/metrics.json new file mode 100644 index 0000000..d373883 --- /dev/null +++ b/codemods/replace-string-ref/tests/non-identifier-ref-name/metrics.json @@ -0,0 +1,11 @@ +{ + "string-ref-replacements": [ + { + "cardinality": { + "file": "tests/non-identifier-ref-name/input.tsx", + "refs": "foo-bar" + }, + "count": 1 + } + ] +} \ No newline at end of file From 19c9408e1853ab394ed652a6e9271f9bba273a65 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 19:06:57 +0200 Subject: [PATCH 19/29] fix replace-act-import migrate all named act imports --- .../replace-act-import/scripts/codemod.ts | 166 ++++++++++++++---- .../expected.tsx | 7 + .../input.tsx | 6 + .../metrics.json | 11 ++ .../tests/multiple-named-imports/expected.tsx | 6 + .../tests/multiple-named-imports/input.tsx | 6 + .../tests/multiple-named-imports/metrics.json | 11 ++ 7 files changed, 182 insertions(+), 31 deletions(-) create mode 100644 codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx create mode 100644 codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/input.tsx create mode 100644 codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/metrics.json create mode 100644 codemods/replace-act-import/tests/multiple-named-imports/expected.tsx create mode 100644 codemods/replace-act-import/tests/multiple-named-imports/input.tsx create mode 100644 codemods/replace-act-import/tests/multiple-named-imports/metrics.json diff --git a/codemods/replace-act-import/scripts/codemod.ts b/codemods/replace-act-import/scripts/codemod.ts index bbaee28..cfb5346 100644 --- a/codemods/replace-act-import/scripts/codemod.ts +++ b/codemods/replace-act-import/scripts/codemod.ts @@ -1,7 +1,7 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport, addImport, removeImport } from "@jssg/utils/javascript/imports"; +import { getImport, removeImport } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; @@ -12,6 +12,75 @@ const TEST_UTILS_MODULE = "react-dom/test-utils"; const REACT_MODULE = "react"; type TestUtilsImport = { name: string; type: "default" | "namespace" }; +type NamedActImport = { importNode: SgNode; specifier: SgNode; alias: string | null }; + +function sourceText(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = node.text(); + if (text.length >= 2) return text.slice(1, -1); + return null; +} + +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? sourceText(source) : null; +} + +function isWhitespace(char: string | undefined): boolean { + return char === " " || char === "\t" || char === "\n" || char === "\r"; +} + +function removeNodeWithComma(node: SgNode, source: string): Edit { + let start = node.range().start.index; + let end = node.range().end.index; + + let i = end; + while (isWhitespace(source[i])) i++; + if (source[i] === ",") { + end = i + 1; + while (isWhitespace(source[end])) end++; + } else { + let j = start - 1; + while (isWhitespace(source[j])) j--; + if (source[j] === ",") start = j; + } + + return { startPos: start, endPos: end, insertedText: "" }; +} + +function namedImportText(alias: string | null): string { + return alias && alias !== "act" ? `act as ${alias}` : "act"; +} + +function countImportSpecifiers(importNode: SgNode): number { + return importNode.findAll({ rule: { kind: "import_specifier" } }).length; +} + +function findExistingReactNamedImport(rootNode: SgNode): SgNode | null { + return rootNode.findAll({ rule: { kind: "import_statement" } }).find((imp) => + importSource(imp) === REACT_MODULE && imp.find({ rule: { kind: "named_imports" } }) !== null + ) ?? null; +} + +function findNamedActImports(rootNode: SgNode): NamedActImport[] { + const imports: NamedActImport[] = []; + + for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (importSource(importNode) !== TEST_UTILS_MODULE) continue; + + for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { + if (specifier.field("name")?.text() !== "act") continue; + imports.push({ + importNode, + specifier, + alias: specifier.field("alias")?.text() ?? null, + }); + } + } + + return imports; +} function getTestUtilsImport(rootNode: SgNode): (TestUtilsImport & { importNode?: SgNode }) | null { const defaultImp = getImport(rootNode, { type: "default", from: TEST_UTILS_MODULE }); @@ -48,11 +117,12 @@ function getTestUtilsImport(rootNode: SgNode): (TestUtilsImport const transform: Transform = async (root) => { const rootNode = root.root(); + const source = rootNode.text(); const edits: Edit[] = []; const metric = useMetricAtom("replace-act-import-migrations"); const testUtilsImport = getTestUtilsImport(rootNode); - const actNamedImport = getImport(rootNode, { type: "named", name: "act", from: TEST_UTILS_MODULE }); + const namedActImports = findNamedActImports(rootNode); if (testUtilsImport) { const actMemberCalls = rootNode.findAll({ @@ -107,44 +177,78 @@ const transform: Transform = async (root) => { } } - if (actNamedImport && !actNamedImport.isNamespace) { - const importNode = actNamedImport.node.ancestors().find((a) => a.kind() === "import_statement"); - const specifierCount = importNode?.findAll({ rule: { kind: "import_specifier" } }).length ?? 0; - if (importNode && specifierCount === 1) { - const alias = - actNamedImport.alias && actNamedImport.alias !== "act" - ? `act as ${actNamedImport.alias}` - : "act"; - const existingReactNamedImport = rootNode.findAll({ rule: { kind: "import_statement" } }).find((imp) => - imp.find({ rule: { kind: "string", has: { kind: "string_fragment", regex: "^react$" } } }) !== null && - imp.find({ rule: { kind: "named_imports" } }) !== null - ); - if (existingReactNamedImport) { + if (namedActImports.length > 0) { + const existingReactNamedImport = findExistingReactNamedImport(rootNode); + const existingReactActSpecifiers = new Set(); + if (existingReactNamedImport) { + for (const specifier of existingReactNamedImport.findAll({ rule: { kind: "import_specifier" } })) { + if (specifier.field("name")?.text() !== "act") continue; + existingReactActSpecifiers.add(namedImportText(specifier.field("alias")?.text() ?? null)); + } + } + + const wantedActSpecifiers = Array.from( + new Set(namedActImports.map((entry) => namedImportText(entry.alias))), + ); + const specifiersToAdd = wantedActSpecifiers.filter((text) => !existingReactActSpecifiers.has(text)); + + let replacementImportId: number | null = null; + if (existingReactNamedImport) { + if (specifiersToAdd.length > 0) { const namedImports = existingReactNamedImport.find({ rule: { kind: "named_imports" } }); if (namedImports) { const insertPos = namedImports.range().end.index - 1; - edits.push({ startPos: insertPos, endPos: insertPos, insertedText: `, ${alias}` }); + edits.push({ + startPos: insertPos, + endPos: insertPos, + insertedText: `, ${specifiersToAdd.join(", ")}`, + }); } - edits.push(importNode.replace("")); + } + } else if (specifiersToAdd.length > 0) { + const firstNamedActImport = namedActImports[0]!.importNode; + const firstImportActCount = namedActImports.filter((entry) => + entry.importNode.id() === firstNamedActImport.id(), + ).length; + const replacementText = `import { ${specifiersToAdd.join(", ")} } from "react";`; + + if (countImportSpecifiers(firstNamedActImport) === firstImportActCount) { + edits.push(firstNamedActImport.replace(replacementText)); + replacementImportId = firstNamedActImport.id(); } else { - edits.push(importNode.replace(`import { ${alias} } from "react";`)); + edits.push({ + startPos: firstNamedActImport.range().start.index, + endPos: firstNamedActImport.range().start.index, + insertedText: `${replacementText}\n`, + }); + } + } + const importsById = new Map; specifiers: SgNode[] }>(); + for (const entry of namedActImports) { + const current = importsById.get(entry.importNode.id()); + if (current) { + current.specifiers.push(entry.specifier); + } else { + importsById.set(entry.importNode.id(), { + importNode: entry.importNode, + specifiers: [entry.specifier], + }); } - } else { - const addEdit = addImport(rootNode, { - type: "named", - specifiers: [{ name: "act", alias: actNamedImport.alias }], - from: REACT_MODULE, - }); - if (addEdit) edits.push(addEdit); + } - const removeEdit = removeImport(rootNode, { - type: "named", - specifiers: ["act"], - from: TEST_UTILS_MODULE, - }); - if (removeEdit) edits.push(removeEdit); + for (const [importId, group] of importsById) { + if (replacementImportId === importId) continue; + + if (countImportSpecifiers(group.importNode) === group.specifiers.length) { + edits.push(group.importNode.replace("")); + } else { + for (const specifier of group.specifiers) { + edits.push(removeNodeWithComma(specifier, source)); + } + } } + metric.increment({ file: metricFile(root.filename()), pattern: "named-import" }); } diff --git a/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx b/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx new file mode 100644 index 0000000..7f9f63a --- /dev/null +++ b/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx @@ -0,0 +1,7 @@ +import { act, act as act2 } from "react"; +import { render } from "react-dom/test-utils"; + + +render(); +act(); +act2(); diff --git a/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/input.tsx b/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/input.tsx new file mode 100644 index 0000000..b5a65bb --- /dev/null +++ b/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/input.tsx @@ -0,0 +1,6 @@ +import { render, act } from "react-dom/test-utils"; +import { act as act2 } from "react-dom/test-utils"; + +render(); +act(); +act2(); diff --git a/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/metrics.json b/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/metrics.json new file mode 100644 index 0000000..1fbe435 --- /dev/null +++ b/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-act-import-migrations": [ + { + "cardinality": { + "file": "tests/multiple-named-imports-with-other-specifier/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-act-import/tests/multiple-named-imports/expected.tsx b/codemods/replace-act-import/tests/multiple-named-imports/expected.tsx new file mode 100644 index 0000000..dec384a --- /dev/null +++ b/codemods/replace-act-import/tests/multiple-named-imports/expected.tsx @@ -0,0 +1,6 @@ +import { FC , act, act as act2} from "react"; + + + +act(); +act2(); diff --git a/codemods/replace-act-import/tests/multiple-named-imports/input.tsx b/codemods/replace-act-import/tests/multiple-named-imports/input.tsx new file mode 100644 index 0000000..3b9408c --- /dev/null +++ b/codemods/replace-act-import/tests/multiple-named-imports/input.tsx @@ -0,0 +1,6 @@ +import { FC } from "react"; +import { act } from "react-dom/test-utils"; +import { act as act2 } from "react-dom/test-utils"; + +act(); +act2(); diff --git a/codemods/replace-act-import/tests/multiple-named-imports/metrics.json b/codemods/replace-act-import/tests/multiple-named-imports/metrics.json new file mode 100644 index 0000000..a727e22 --- /dev/null +++ b/codemods/replace-act-import/tests/multiple-named-imports/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-act-import-migrations": [ + { + "cardinality": { + "file": "tests/multiple-named-imports/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file From 422d89fe6894ba61fc6d6ea4a5f1312957cf9862 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 19:39:20 +0200 Subject: [PATCH 20/29] fix issues --- .../scripts/codemod.ts | 187 +++++++++-------- .../expected.tsx | 8 + .../multiple-member-render-imports/input.tsx | 5 + .../metrics.json | 11 + .../expected.tsx | 8 + .../multiple-named-render-imports/input.tsx | 5 + .../metrics.json | 11 + .../replace-string-ref/scripts/codemod.ts | 51 ++--- .../mixin-component-no-change/expected.tsx | 2 + .../tests/mixin-component-no-change/input.tsx | 2 + .../replace-use-form-state/scripts/codemod.ts | 127 ++++++------ .../expected.tsx | 3 + .../input.tsx | 3 + .../metrics.json | 11 + .../expected.tsx | 4 +- .../metrics.json | 11 + .../tests/namespace-and-named/expected.tsx | 2 +- .../tests/namespace-and-named/metrics.json | 7 + codemods/use-context-hook/scripts/codemod.ts | 190 +++++++++++------- .../multiple-use-context-imports/expected.tsx | 4 + .../multiple-use-context-imports/input.tsx | 4 + .../multiple-use-context-imports/metrics.json | 18 ++ 22 files changed, 416 insertions(+), 258 deletions(-) create mode 100644 codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx create mode 100644 codemods/replace-reactdom-render/tests/multiple-member-render-imports/input.tsx create mode 100644 codemods/replace-reactdom-render/tests/multiple-member-render-imports/metrics.json create mode 100644 codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx create mode 100644 codemods/replace-reactdom-render/tests/multiple-named-render-imports/input.tsx create mode 100644 codemods/replace-reactdom-render/tests/multiple-named-render-imports/metrics.json create mode 100644 codemods/replace-string-ref/tests/mixin-component-no-change/expected.tsx create mode 100644 codemods/replace-string-ref/tests/mixin-component-no-change/input.tsx create mode 100644 codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx create mode 100644 codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/input.tsx create mode 100644 codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/metrics.json create mode 100644 codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/metrics.json create mode 100644 codemods/use-context-hook/tests/multiple-use-context-imports/expected.tsx create mode 100644 codemods/use-context-hook/tests/multiple-use-context-imports/input.tsx create mode 100644 codemods/use-context-hook/tests/multiple-use-context-imports/metrics.json diff --git a/codemods/replace-reactdom-render/scripts/codemod.ts b/codemods/replace-reactdom-render/scripts/codemod.ts index bd64e14..5fa4bde 100644 --- a/codemods/replace-reactdom-render/scripts/codemod.ts +++ b/codemods/replace-reactdom-render/scripts/codemod.ts @@ -1,4 +1,4 @@ -import type { Transform, Edit } from "codemod:ast-grep"; +import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; import { getImport } from "@jssg/utils/javascript/imports"; @@ -8,6 +8,51 @@ function metricFile(filename: string): string { return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; } +function sourceText(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = node.text(); + return text.length >= 2 ? text.slice(1, -1) : null; +} + +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? sourceText(source) : null; +} + +function findReactDomMemberImportNames(rootNode: SgNode): Set { + const names = new Set(); + + for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (importSource(imp) !== "react-dom") continue; + + const importClause = imp.find({ rule: { kind: "import_clause" } }); + const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); + if (defaultIdentifier) names.add(defaultIdentifier.text()); + + const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); + const namespaceName = + namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); + if (namespaceName) names.add(namespaceName.text()); + } + + return names; +} + +function findNamedRenderImportNames(rootNode: SgNode): Set { + const names = new Set(); + + for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (importSource(importNode) !== "react-dom") continue; + + for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { + if (specifier.field("name")?.text() !== "render") continue; + names.add(specifier.field("alias")?.text() ?? "render"); + } + } + + return names; +} const transform: Transform = async (root) => { const rootNode = root.root(); @@ -15,16 +60,13 @@ const transform: Transform = async (root) => { const transformMetric = useMetricAtom("reactdom-render-replacements"); - const reactDomDefaultImport = getImport(rootNode, { type: "default", from: "react-dom" }); - const reactDomRenderImport = getImport(rootNode, { type: "named", name: "render", from: "react-dom" }); + const reactDomMemberImportNames = findReactDomMemberImportNames(rootNode); + const reactDomRenderImportNames = findNamedRenderImportNames(rootNode); - if (!reactDomDefaultImport && !reactDomRenderImport) { + if (reactDomMemberImportNames.size === 0 && reactDomRenderImportNames.size === 0) { return null; } - const reactDomName = reactDomDefaultImport?.alias || "ReactDOM"; - const renderName = reactDomRenderImport?.alias || "render"; - let hasTransformations = false; let rootIndex = 0; @@ -34,7 +76,46 @@ const transform: Transform = async (root) => { return name; }; - if (reactDomDefaultImport) { + const sourceCode = rootNode.text(); + + const applyRenderReplacement = (call: SgNode, pattern: string): void => { + const args = call.field("arguments"); + if (!args) return; + + const argList = args.children().filter((child) => + child.kind() !== "(" && child.kind() !== ")" && child.kind() !== "," + ); + + if (argList.length < 2) return; + + const element = argList[0]!; + const container = argList[1]!; + const statement = call.ancestors().find((ancestor) => ancestor.kind() === "expression_statement"); + if (!statement) return; + + const rootName = nextRootName(); + const stmtStart = statement.range().start.index; + let indent = ""; + for (let i = stmtStart - 1; i >= 0; i--) { + const char = sourceCode[i]; + if (char === "\n") break; + if (char === " " || char === "\t") { + indent = char + indent; + } else { + indent = ""; + } + } + + const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${rootName}.render(${element.text()});`; + edits.push(statement.replace(replacement)); + hasTransformations = true; + transformMetric.increment({ + pattern, + file: metricFile(root.filename()), + }); + }; + + if (reactDomMemberImportNames.size > 0) { const memberRenderCalls = rootNode.findAll({ rule: { kind: "call_expression", @@ -46,7 +127,7 @@ const transform: Transform = async (root) => { has: { field: "object", kind: "identifier", - regex: `^${reactDomName}$`, + regex: ".*", }, }, { @@ -62,98 +143,29 @@ const transform: Transform = async (root) => { }); for (const call of memberRenderCalls) { - const args = call.field("arguments"); - if (!args) continue; - - const argList = args.children().filter((child) => - child.kind() !== "(" && child.kind() !== ")" && child.kind() !== "," - ); - - if (argList.length >= 2) { - const element = argList[0]!; - const container = argList[1]!; - - const statement = call.ancestors().find((ancestor) => - ancestor.kind() === "expression_statement" - ); - - if (statement) { - const rootName = nextRootName(); - const sourceCode = rootNode.text(); - const stmtStart = statement.range().start.index; - let indent = ""; - for (let i = stmtStart - 1; i >= 0; i--) { - const char = sourceCode[i]; - if (char === '\n') break; - if (char === ' ' || char === '\t') { - indent = char + indent; - } else { - indent = ""; - } - } - const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${rootName}.render(${element.text()});`; - edits.push(statement.replace(replacement)); - hasTransformations = true; - transformMetric.increment({ - pattern: "ReactDOM.render", - file: metricFile(root.filename()), - }); - } - } + const callee = call.field("function"); + const objectNode = callee?.field("object"); + if (!objectNode || !reactDomMemberImportNames.has(objectNode.text())) continue; + applyRenderReplacement(call, "ReactDOM.render"); } } - if (reactDomRenderImport) { + if (reactDomRenderImportNames.size > 0) { const renderCalls = rootNode.findAll({ rule: { kind: "call_expression", has: { field: "function", kind: "identifier", - regex: `^${renderName}$`, + regex: ".*", }, }, }); for (const call of renderCalls) { - const args = call.field("arguments"); - if (!args) continue; - - const argList = args.children().filter((child) => - child.kind() !== "(" && child.kind() !== ")" && child.kind() !== "," - ); - - if (argList.length >= 2) { - const element = argList[0]!; - const container = argList[1]!; - - const statement = call.ancestors().find((ancestor) => - ancestor.kind() === "expression_statement" - ); - - if (statement) { - const rootName = nextRootName(); - const sourceCode = rootNode.text(); - const stmtStart = statement.range().start.index; - let indent = ""; - for (let i = stmtStart - 1; i >= 0; i--) { - const char = sourceCode[i]; - if (char === '\n') break; - if (char === ' ' || char === '\t') { - indent = char + indent; - } else { - indent = ""; - } - } - const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${rootName}.render(${element.text()});`; - edits.push(statement.replace(replacement)); - hasTransformations = true; - transformMetric.increment({ - pattern: "render", - file: metricFile(root.filename()), - }); - } - } + const callee = call.field("function"); + if (!callee || !reactDomRenderImportNames.has(callee.text())) continue; + applyRenderReplacement(call, "render"); } } @@ -171,7 +183,6 @@ const transform: Transform = async (root) => { startPos: 0, endPos: 0, insertedText: 'import { createRoot } from "react-dom/client";\n', - }); } diff --git a/codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx b/codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx new file mode 100644 index 0000000..b9416e9 --- /dev/null +++ b/codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx @@ -0,0 +1,8 @@ +import { createRoot } from "react-dom/client"; +import ReactDOM from "react-dom"; +import * as DOM from "react-dom"; + +const root = createRoot(node1); +root.render(); +const root1 = createRoot(node2); +root1.render(); diff --git a/codemods/replace-reactdom-render/tests/multiple-member-render-imports/input.tsx b/codemods/replace-reactdom-render/tests/multiple-member-render-imports/input.tsx new file mode 100644 index 0000000..b100d69 --- /dev/null +++ b/codemods/replace-reactdom-render/tests/multiple-member-render-imports/input.tsx @@ -0,0 +1,5 @@ +import ReactDOM from "react-dom"; +import * as DOM from "react-dom"; + +ReactDOM.render(, node1); +DOM.render(, node2); diff --git a/codemods/replace-reactdom-render/tests/multiple-member-render-imports/metrics.json b/codemods/replace-reactdom-render/tests/multiple-member-render-imports/metrics.json new file mode 100644 index 0000000..abafe95 --- /dev/null +++ b/codemods/replace-reactdom-render/tests/multiple-member-render-imports/metrics.json @@ -0,0 +1,11 @@ +{ + "reactdom-render-replacements": [ + { + "cardinality": { + "file": "tests/multiple-member-render-imports/input.tsx", + "pattern": "ReactDOM.render" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx b/codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx new file mode 100644 index 0000000..4a7b7a9 --- /dev/null +++ b/codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx @@ -0,0 +1,8 @@ +import { createRoot } from "react-dom/client"; +import { render } from "react-dom"; +import { render as render2 } from "react-dom"; + +const root = createRoot(node1); +root.render(); +const root1 = createRoot(node2); +root1.render(); diff --git a/codemods/replace-reactdom-render/tests/multiple-named-render-imports/input.tsx b/codemods/replace-reactdom-render/tests/multiple-named-render-imports/input.tsx new file mode 100644 index 0000000..2ee1dad --- /dev/null +++ b/codemods/replace-reactdom-render/tests/multiple-named-render-imports/input.tsx @@ -0,0 +1,5 @@ +import { render } from "react-dom"; +import { render as render2 } from "react-dom"; + +render(, node1); +render2(, node2); diff --git a/codemods/replace-reactdom-render/tests/multiple-named-render-imports/metrics.json b/codemods/replace-reactdom-render/tests/multiple-named-render-imports/metrics.json new file mode 100644 index 0000000..49c0961 --- /dev/null +++ b/codemods/replace-reactdom-render/tests/multiple-named-render-imports/metrics.json @@ -0,0 +1,11 @@ +{ + "reactdom-render-replacements": [ + { + "cardinality": { + "file": "tests/multiple-named-render-imports/input.tsx", + "pattern": "render" + }, + "count": 2 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-string-ref/scripts/codemod.ts b/codemods/replace-string-ref/scripts/codemod.ts index 54c8b5e..66885df 100644 --- a/codemods/replace-string-ref/scripts/codemod.ts +++ b/codemods/replace-string-ref/scripts/codemod.ts @@ -19,39 +19,28 @@ function isInsideReactClassComponent( const heritage = classDecl.children().find((c) => c.kind() === "class_heritage"); if (!heritage) return false; - - // Check for React.Component or React.PureComponent - const memberExpressions = heritage.findAll({ - rule: { kind: "member_expression" }, - }); - - for (const memberExpr of memberExpressions) { - const object = memberExpr.field("object"); - const property = memberExpr.field("property"); - - if (object && property) { - const objectText = object.text(); - const propertyText = property.text(); - - if (objectText === reactName && (propertyText === "Component" || propertyText === "PureComponent")) { - return true; - } - } + + const extendsClause = heritage.children().find((child) => child.kind() === "extends_clause"); + const directSuperclass = extendsClause?.children().find( + (child) => child.isNamed() && child.kind() !== "type_arguments", + ); + if (!directSuperclass) return false; + + if (directSuperclass.kind() === "member_expression") { + const object = directSuperclass.field("object"); + const property = directSuperclass.field("property"); + return !!( + object && + property && + object.text() === reactName && + (property.text() === "Component" || property.text() === "PureComponent") + ); } - - // Check for named imports like Component or PureComponent - if (componentNamesList.length > 0) { - const identifiers = heritage.findAll({ - rule: { kind: "identifier" }, - }); - - for (const id of identifiers) { - if (componentNamesList.includes(id.text())) { - return true; - } - } + + if (directSuperclass.kind() === "identifier") { + return componentNamesList.includes(directSuperclass.text()); } - + return false; } diff --git a/codemods/replace-string-ref/tests/mixin-component-no-change/expected.tsx b/codemods/replace-string-ref/tests/mixin-component-no-change/expected.tsx new file mode 100644 index 0000000..7eda311 --- /dev/null +++ b/codemods/replace-string-ref/tests/mixin-component-no-change/expected.tsx @@ -0,0 +1,2 @@ +import { Component } from "react"; +class C extends mixin(Component) { render(){ return
    ; } } diff --git a/codemods/replace-string-ref/tests/mixin-component-no-change/input.tsx b/codemods/replace-string-ref/tests/mixin-component-no-change/input.tsx new file mode 100644 index 0000000..7eda311 --- /dev/null +++ b/codemods/replace-string-ref/tests/mixin-component-no-change/input.tsx @@ -0,0 +1,2 @@ +import { Component } from "react"; +class C extends mixin(Component) { render(){ return
    ; } } diff --git a/codemods/replace-use-form-state/scripts/codemod.ts b/codemods/replace-use-form-state/scripts/codemod.ts index 8425623..79542ba 100644 --- a/codemods/replace-use-form-state/scripts/codemod.ts +++ b/codemods/replace-use-form-state/scripts/codemod.ts @@ -11,54 +11,56 @@ function metricFile(filename: string): string { const REACT_DOM_MODULE = "react-dom"; -type ReactDOMImport = { name: string; type: "default" | "namespace" }; +type NamedUseFormStateImport = { importNode: SgNode; specifier: SgNode; localName: string }; -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - if (!source) return null; - const fragment = source.find({ rule: { kind: "string_fragment" } }); +function sourceText(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); if (fragment) return fragment.text(); - const text = source.text(); + const text = node.text(); return text.length >= 2 ? text.slice(1, -1) : null; } -function getReactDOMImport(rootNode: SgNode): ReactDOMImport | null { - const defaultImp = getImport(rootNode, { type: "default", from: REACT_DOM_MODULE }); - if (defaultImp) return { name: defaultImp.alias, type: "default" }; - - const importDecls = rootNode.findAll({ - rule: { - kind: "import_statement", - has: { - all: [ - { kind: "string", regex: "^react-dom$" }, - { kind: "namespace_import" }, - ], - }, - }, - }); +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? sourceText(source) : null; +} + +function findReactDOMMemberImportNames(rootNode: SgNode): Set { + const names = new Set(); + + for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (importSource(imp) !== REACT_DOM_MODULE) continue; + + const importClause = imp.find({ rule: { kind: "import_clause" } }); + const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); + if (defaultIdentifier) names.add(defaultIdentifier.text()); - for (const imp of importDecls) { const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); - if (namespaceImport) { - const ident = namespaceImport.field("name") ?? namespaceImport.find({ rule: { kind: "identifier" } }); - if (ident) return { name: ident.text(), type: "namespace" }; - } + const namespaceName = + namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); + if (namespaceName) names.add(namespaceName.text()); } - return null; -} -function firstReactDOMImport(rootNode: SgNode): SgNode | null { - return rootNode.findAll({ rule: { kind: "import_statement" } }) - .find((node) => importSource(node) === REACT_DOM_MODULE) ?? null; + return names; } -function matchingUseFormStateSpecifier(importNode: SgNode): SgNode | null { - return importNode.findAll({ - rule: { - kind: "import_specifier", - }, - }).find((spec) => spec.field("name")?.text() === "useFormState") ?? null; +function findNamedUseFormStateImports(rootNode: SgNode): NamedUseFormStateImport[] { + const imports: NamedUseFormStateImport[] = []; + + for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (importSource(importNode) !== REACT_DOM_MODULE) continue; + + for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { + if (specifier.field("name")?.text() !== "useFormState") continue; + imports.push({ + importNode, + specifier, + localName: specifier.field("alias")?.text() ?? specifier.field("name")?.text() ?? "useFormState", + }); + } + } + + return imports; } function replacementImportSpecifierText(usedName: string): string { @@ -72,9 +74,10 @@ const transform: Transform = async (root) => { const edits: Edit[] = []; const metric = useMetricAtom("replace-use-form-state-migrations"); - const reactDOMImport = getReactDOMImport(rootNode); + const reactDOMMemberImportNames = findReactDOMMemberImportNames(rootNode); + const namedUseFormStateImports = findNamedUseFormStateImports(rootNode); - if (reactDOMImport) { + if (reactDOMMemberImportNames.size > 0) { const memberCalls = rootNode.findAll({ rule: { kind: "member_expression", @@ -87,7 +90,7 @@ const transform: Transform = async (root) => { for (const member of memberCalls) { const objNode = member.field("object"); - if (objNode && objNode.text() === reactDOMImport.name) { + if (objNode && reactDOMMemberImportNames.has(objNode.text())) { const propNode = member.field("property"); if (propNode) { edits.push(propNode.replace("useActionState")); @@ -97,33 +100,35 @@ const transform: Transform = async (root) => { } } - const importNode = firstReactDOMImport(rootNode); - const specifier = importNode ? matchingUseFormStateSpecifier(importNode) : null; - if (!specifier) { - if (edits.length === 0) return null; - return rootNode.commitEdits(edits); - } + if (namedUseFormStateImports.length > 0) { + let shouldRenameUnaliasedUses = false; - const localName = specifier.field("alias")?.text() ?? specifier.field("name")?.text() ?? "useFormState"; - edits.push(specifier.replace(replacementImportSpecifierText(localName))); + for (const entry of namedUseFormStateImports) { + edits.push(entry.specifier.replace(replacementImportSpecifierText(entry.localName))); + if (entry.localName === "useFormState") { + shouldRenameUnaliasedUses = true; + } + } - if (localName === "useFormState") { - const renameTargets = rootNode.findAll({ - rule: { - any: [ - { kind: "identifier", regex: "^useFormState$" }, - { kind: "type_identifier", regex: "^useFormState$" }, - ], - }, - }); + if (shouldRenameUnaliasedUses) { + const renameTargets = rootNode.findAll({ + rule: { + any: [ + { kind: "identifier", regex: "^useFormState$" }, + { kind: "type_identifier", regex: "^useFormState$" }, + ], + }, + }); - for (const node of renameTargets) { - if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) continue; - edits.push(node.replace("useActionState")); + for (const node of renameTargets) { + if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) continue; + edits.push(node.replace("useActionState")); + } } + + metric.increment({ file: metricFile(root.filename()), pattern: "named-import" }); } - metric.increment({ file: metricFile(root.filename()), pattern: "named-import" }); if (edits.length === 0) return null; return rootNode.commitEdits(edits); }; diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx new file mode 100644 index 0000000..1d90ff0 --- /dev/null +++ b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx @@ -0,0 +1,3 @@ +import { useActionState } from 'react-dom'; +import { useActionState as ufs } from 'react-dom'; +function C(){ useActionState(a,0); return ufs(b,1); } diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/input.tsx b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/input.tsx new file mode 100644 index 0000000..1363096 --- /dev/null +++ b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/input.tsx @@ -0,0 +1,3 @@ +import { useFormState } from 'react-dom'; +import { useFormState as ufs } from 'react-dom'; +function C(){ useFormState(a,0); return ufs(b,1); } diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/metrics.json b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/metrics.json new file mode 100644 index 0000000..30814d4 --- /dev/null +++ b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-use-form-state-migrations": [ + { + "cardinality": { + "file": "tests/multiple-react-dom-imports-both-have-hook/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx index c3611b0..d17a3d8 100644 --- a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx +++ b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx @@ -1,3 +1,3 @@ import { createPortal } from 'react-dom'; -import { useFormState } from 'react-dom'; -function C(){ return useFormState(a,0); } +import { useActionState } from 'react-dom'; +function C(){ return useActionState(a,0); } diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/metrics.json b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/metrics.json new file mode 100644 index 0000000..3b7e6ea --- /dev/null +++ b/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/metrics.json @@ -0,0 +1,11 @@ +{ + "replace-use-form-state-migrations": [ + { + "cardinality": { + "file": "tests/multiple-react-dom-imports-second-has-hook/input.tsx", + "pattern": "named-import" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx b/codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx index 517c6e5..7e7b98a 100644 --- a/codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx +++ b/codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx @@ -1,3 +1,3 @@ import * as ReactDOM from 'react-dom'; -import { useFormState as useFormStateAlias } from 'react-dom'; +import { useActionState as useFormStateAlias } from 'react-dom'; function C(){ const x = useFormStateAlias(a,0); return ReactDOM.useActionState(b,1); } diff --git a/codemods/replace-use-form-state/tests/namespace-and-named/metrics.json b/codemods/replace-use-form-state/tests/namespace-and-named/metrics.json index f922361..507ddec 100644 --- a/codemods/replace-use-form-state/tests/namespace-and-named/metrics.json +++ b/codemods/replace-use-form-state/tests/namespace-and-named/metrics.json @@ -6,6 +6,13 @@ "pattern": "member-call" }, "count": 1 + }, + { + "cardinality": { + "file": "tests/namespace-and-named/input.tsx", + "pattern": "named-import" + }, + "count": 1 } ] } \ No newline at end of file diff --git a/codemods/use-context-hook/scripts/codemod.ts b/codemods/use-context-hook/scripts/codemod.ts index eb3a2cb..7e27b57 100644 --- a/codemods/use-context-hook/scripts/codemod.ts +++ b/codemods/use-context-hook/scripts/codemod.ts @@ -1,13 +1,62 @@ import type { Transform, Edit, SgNode } from "codemod:ast-grep"; import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; function metricFile(filename: string): string { const cwd = process.cwd() + "/"; return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; } +type NamedUseContextImport = { specifier: SgNode; localName: string }; + +function sourceText(node: SgNode): string | null { + const fragment = node.find({ rule: { kind: "string_fragment" } }); + if (fragment) return fragment.text(); + const text = node.text(); + return text.length >= 2 ? text.slice(1, -1) : null; +} + +function importSource(node: SgNode): string | null { + const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); + return source ? sourceText(source) : null; +} + +function findReactMemberImportNames(rootNode: SgNode): Set { + const names = new Set(); + + for (const imp of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (importSource(imp) !== "react") continue; + + const importClause = imp.find({ rule: { kind: "import_clause" } }); + const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); + if (defaultIdentifier) names.add(defaultIdentifier.text()); + + const namespaceImport = imp.find({ rule: { kind: "namespace_import" } }); + const namespaceName = + namespaceImport?.field("name") ?? namespaceImport?.find({ rule: { kind: "identifier" } }); + if (namespaceName) names.add(namespaceName.text()); + } + + return names; +} + +function findNamedUseContextImports(rootNode: SgNode): NamedUseContextImport[] { + const imports: NamedUseContextImport[] = []; + + for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { + if (importSource(importNode) !== "react") continue; + + for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { + if (specifier.field("name")?.text() !== "useContext") continue; + imports.push({ + specifier, + localName: specifier.field("alias")?.text() ?? "useContext", + }); + } + } + + return imports; +} const transform: Transform = async (root) => { const rootNode = root.root(); @@ -15,13 +64,8 @@ const transform: Transform = async (root) => { const transformMetric = useMetricAtom("use-context-transformations"); - const reactDefaultImport = getImport(rootNode, { type: "default", from: "react" }); - const reactDefaultName = reactDefaultImport?.alias || "React"; - - const useContextImport = getImport(rootNode, { type: "named", name: "useContext", from: "react" }); - const useContextLocalName = useContextImport?.alias; - - let needsUseNamedImport = false; + const reactMemberImportNames = findReactMemberImportNames(rootNode); + const namedUseContextImports = findNamedUseContextImports(rootNode); const shouldSkipLegacyIdentifierRename = (node: SgNode): boolean => { const parent = node.parent(); @@ -35,7 +79,7 @@ const transform: Transform = async (root) => { } return false; }; - + const reactMemberCalls = rootNode.findAll({ rule: { kind: "call_expression", @@ -47,7 +91,7 @@ const transform: Transform = async (root) => { has: { field: "object", kind: "identifier", - regex: `^${reactDefaultName}$`, + regex: ".*", }, }, { @@ -62,91 +106,87 @@ const transform: Transform = async (root) => { }, }); + let reactMemberCallCount = 0; for (const call of reactMemberCalls) { - const propertyNode = call.find({ - rule: { - kind: "property_identifier", - regex: "^useContext$", - }, - }); + const functionNode = call.field("function"); + const objectNode = functionNode?.field("object"); + if (!objectNode || !reactMemberImportNames.has(objectNode.text())) continue; - if (propertyNode) { - edits.push(propertyNode.replace("use")); - transformMetric.increment({ - pattern: "React.useContext", - file: metricFile(root.filename()) - }); - } + const propertyNode = functionNode?.field("property"); + if (!propertyNode) continue; + + edits.push(propertyNode.replace("use")); + reactMemberCallCount += 1; } - if (useContextLocalName) { + if (reactMemberCallCount > 0) { + transformMetric.increment({ + pattern: "React.useContext", + file: metricFile(root.filename()), + }, reactMemberCallCount); + } + + const activeLocalNames = new Map(); + for (const localName of new Set(namedUseContextImports.map((entry) => entry.localName))) { const useContextCalls = rootNode.findAll({ rule: { kind: "call_expression", has: { field: "function", kind: "identifier", - regex: `^${useContextLocalName}$`, + regex: `^${localName}$`, }, }, }); - const hasUseContextCalls = useContextCalls.length > 0; - - if (hasUseContextCalls) { - const isAliased = useContextImport && useContextImport.alias !== "useContext"; - - if (!isAliased) { - const renameTargets = rootNode.findAll({ - rule: { - any: [ - { kind: "identifier", regex: `^${useContextLocalName}$` }, - { kind: "type_identifier", regex: `^${useContextLocalName}$` }, - ], - }, - }); - - for (const node of renameTargets) { - if (shouldSkipLegacyIdentifierRename(node)) continue; - edits.push(node.replace("use")); - } - transformMetric.increment({ - pattern: "useContext", - file: metricFile(root.filename()) - }, useContextCalls.length); - } else { - transformMetric.increment({ - pattern: "useContext (aliased)", - file: metricFile(root.filename()) - }, useContextCalls.length); - } - - needsUseNamedImport = true; + if (useContextCalls.length > 0) { + activeLocalNames.set(localName, useContextCalls.length); } } - if (needsUseNamedImport && useContextImport) { - const importStatement = useContextImport.node.ancestors().find( - (a) => a.kind() === "import_statement" - ); - - if (importStatement) { - const importText = importStatement.text(); - let newImportText: string; - - if (useContextImport.alias !== "useContext") { - newImportText = importText.replace( - new RegExp(`\\buseContext\\s+as\\s+${useContextImport.alias}\\b`), - `use as ${useContextImport.alias}` - ); - } else { - newImportText = importText.replace(/\buseContext\b/, "use"); - } - - edits.push(importStatement.replace(newImportText)); + if (activeLocalNames.has("useContext")) { + const renameTargets = rootNode.findAll({ + rule: { + any: [ + { kind: "identifier", regex: "^useContext$" }, + { kind: "type_identifier", regex: "^useContext$" }, + ], + }, + }); + + for (const node of renameTargets) { + if (shouldSkipLegacyIdentifierRename(node)) continue; + edits.push(node.replace("use")); } } + for (const entry of namedUseContextImports) { + if (!activeLocalNames.has(entry.localName)) continue; + const replacementText = entry.localName === "useContext" ? "use" : `use as ${entry.localName}`; + edits.push(entry.specifier.replace(replacementText)); + } + + const unaliasedCallCount = activeLocalNames.get("useContext") ?? 0; + if (unaliasedCallCount > 0) { + transformMetric.increment({ + pattern: "useContext", + file: metricFile(root.filename()), + }, unaliasedCallCount); + } + + let aliasedCallCount = 0; + for (const [localName, count] of activeLocalNames) { + if (localName === "useContext") continue; + aliasedCallCount += count; + } + + if (aliasedCallCount > 0) { + transformMetric.increment({ + pattern: "useContext (aliased)", + file: metricFile(root.filename()), + }, aliasedCallCount); + } + if (edits.length === 0) { return null; } diff --git a/codemods/use-context-hook/tests/multiple-use-context-imports/expected.tsx b/codemods/use-context-hook/tests/multiple-use-context-imports/expected.tsx new file mode 100644 index 0000000..7526834 --- /dev/null +++ b/codemods/use-context-hook/tests/multiple-use-context-imports/expected.tsx @@ -0,0 +1,4 @@ +import { use } from "react"; +import { use as uc } from "react"; +const a = use(C1); +const b = uc(C2); diff --git a/codemods/use-context-hook/tests/multiple-use-context-imports/input.tsx b/codemods/use-context-hook/tests/multiple-use-context-imports/input.tsx new file mode 100644 index 0000000..17cd21a --- /dev/null +++ b/codemods/use-context-hook/tests/multiple-use-context-imports/input.tsx @@ -0,0 +1,4 @@ +import { useContext } from "react"; +import { useContext as uc } from "react"; +const a = useContext(C1); +const b = uc(C2); diff --git a/codemods/use-context-hook/tests/multiple-use-context-imports/metrics.json b/codemods/use-context-hook/tests/multiple-use-context-imports/metrics.json new file mode 100644 index 0000000..4d18366 --- /dev/null +++ b/codemods/use-context-hook/tests/multiple-use-context-imports/metrics.json @@ -0,0 +1,18 @@ +{ + "use-context-transformations": [ + { + "cardinality": { + "file": "tests/multiple-use-context-imports/input.tsx", + "pattern": "useContext" + }, + "count": 1 + }, + { + "cardinality": { + "file": "tests/multiple-use-context-imports/input.tsx", + "pattern": "useContext (aliased)" + }, + "count": 1 + } + ] +} \ No newline at end of file From e7851824260ef4d02896beaf0b0c80fa4edb3c08 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 20:17:24 +0200 Subject: [PATCH 21/29] refactor repo --- .github/workflows/ci.yml | 48 + LEGACY.md | 40 + PARITY_STATUS.md | 41 + README.md | 56 +- codemods/create-element-to-jsx/README.md | 16 - codemods/create-element-to-jsx/codemod.yaml | 19 - codemods/create-element-to-jsx/package.json | 15 - .../create-element-to-jsx/scripts/codemod.ts | 1277 --- .../scripts/differential-tests.mjs | 69 - .../scripts/error-tests.mjs | 47 - .../allow-member-expression/metrics.json | 10 - .../tests/arg-spread/metrics.json | 10 - .../tests/call-as-children/metrics.json | 10 - .../call-expression-as-prop/metrics.json | 10 - .../tests/children-literal/metrics.json | 10 - .../tests/children-map/metrics.json | 10 - .../children-mixed-empty-string/metrics.json | 10 - .../tests/children/metrics.json | 10 - .../expected.tsx | 4 - .../comment-only-props-string-child/input.tsx | 2 - .../metrics.json | 10 - .../tests/deep-nesting/metrics.json | 10 - .../element-comment-positioning/metrics.json | 10 - .../tests/escaped-string/metrics.json | 10 - .../tests/gt-lt-entities/metrics.json | 10 - .../ignore-bad-capitalization/metrics.json | 10 - .../tests/literal-prop/metrics.json | 10 - .../tests/literal-spacing/metrics.json | 10 - .../member-child-comments-alt/expected.tsx | 4 - .../tests/member-child-comments-alt/input.tsx | 2 - .../member-child-comments-alt/metrics.json | 10 - .../member-expression-as-prop/metrics.json | 10 - .../tests/member-simple-comments/expected.tsx | 2 - .../tests/member-simple-comments/input.tsx | 2 - .../tests/member-simple-comments/metrics.json | 10 - .../tests/no-props-arg/metrics.json | 10 - .../expected.tsx | 3 - .../no-react-valid-explicit-require/input.tsx | 3 - .../metrics.json | 10 - .../test.config.json | 5 - .../tests/no-react-valid/expected.tsx | 3 - .../tests/no-react-valid/input.tsx | 3 - .../tests/no-react/input.tsx | 3 - .../tests/object-assign/metrics.json | 10 - .../tests/preserve-comments/metrics.json | 10 - .../tests/props-array/metrics.json | 10 - .../tests/props-boolean/metrics.json | 10 - .../tests/props/metrics.json | 10 - .../tests/react-spread/metrics.json | 10 - .../tests/single-element/metrics.json | 10 - .../tests/spread-prop-comments/expected.tsx | 4 - .../tests/spread-prop-comments/input.tsx | 2 - .../tests/spread-prop-comments/metrics.json | 10 - .../tests/spread-props/metrics.json | 10 - .../tests/spread/metrics.json | 10 - codemods/error-boundaries/README.md | 16 - codemods/error-boundaries/codemod.yaml | 19 - codemods/error-boundaries/package.json | 15 - codemods/error-boundaries/scripts/codemod.ts | 90 - .../tests/class-component/metrics.json | 10 - .../tests/create-class-component/metrics.json | 10 - codemods/find-dom-node/README.md | 20 - codemods/find-dom-node/codemod.yaml | 19 - codemods/find-dom-node/package.json | 18 - codemods/find-dom-node/scripts/codemod.ts | 175 - .../tests/computed-ref-alias/expected.tsx | 8 - .../tests/computed-ref-alias/input.tsx | 8 - .../tests/computed-ref-alias/metrics.json | 10 - .../tests/deep-refs-chain/expected.tsx | 6 - .../tests/deep-refs-chain/input.tsx | 6 - .../tests/deep-refs-chain/metrics.json | 10 - .../tests/es6-class-no-change/expected.tsx | 7 - .../tests/es6-class-no-change/input.tsx | 7 - .../export-default-no-change/expected.tsx | 7 - .../tests/export-default-no-change/input.tsx | 7 - .../tests/findDOMNode/metrics.json | 10 - .../tests/helper-no-change/expected.tsx | 2 - .../tests/helper-no-change/input.tsx | 2 - .../tests/module-exports/expected.tsx | 7 - .../tests/module-exports/input.tsx | 7 - .../tests/module-exports/metrics.json | 10 - .../expected.tsx | 5 - .../input.tsx | 5 - .../metrics.json | 10 - .../test.config.json | 5 - .../tests/no-react-create-class/expected.tsx | 5 - .../tests/no-react-create-class/input.tsx | 5 - .../react-proptypes-to-prop-types/README.md | 20 + .../codemod.yaml | 0 .../package.json | 0 .../scripts/codemod.ts | 0 .../already-migrated-import/expected.tsx | 0 .../tests/already-migrated-import/input.tsx | 0 .../already-migrated-import/metrics.json | 0 .../expected.tsx | 0 .../input.tsx | 0 .../metrics.json | 0 .../expected.tsx | 0 .../already-migrated-named-import/input.tsx | 0 .../metrics.json | 0 .../already-migrated-require/expected.tsx | 0 .../tests/already-migrated-require/input.tsx | 0 .../already-migrated-require/metrics.json | 0 .../assigned-from-react-var/expected.tsx | 0 .../tests/assigned-from-react-var/input.tsx | 0 .../assigned-from-react-var/metrics.json | 0 .../expected.tsx | 0 .../input.tsx | 0 .../metrics.json | 0 .../default-and-named-import/expected.tsx | 0 .../tests/default-and-named-import/input.tsx | 0 .../tests/default-import/expected.tsx | 0 .../tests/default-import/input.tsx | 0 .../tests/default-import/metrics.json | 0 .../expected.tsx | 0 .../destructured-proptypes-import/input.tsx | 0 .../tests/import-alias/expected.tsx | 0 .../tests/import-alias/input.tsx | 0 .../expected.tsx | 0 .../import-flow-type-with-require/input.tsx | 0 .../mixed-import-and-require-2/expected.tsx | 0 .../mixed-import-and-require-2/input.tsx | 0 .../mixed-import-and-require/expected.tsx | 0 .../tests/mixed-import-and-require/input.tsx | 0 .../mixed-import-and-require/metrics.json | 0 .../tests/module-name/expected.tsx | 0 .../tests/module-name/input.tsx | 0 .../tests/module-name/metrics.json | 0 .../tests/module-name/test.config.json | 0 .../tests/named-parameters/expected.tsx | 0 .../tests/named-parameters/input.tsx | 0 .../expected.tsx | 0 .../input.tsx | 0 .../tests/no-change-import/expected.tsx | 0 .../tests/no-change-import/input.tsx | 0 .../tests/no-change-require/expected.tsx | 0 .../tests/no-change-require/input.tsx | 0 .../tests/require-alias/expected.tsx | 0 .../tests/require-alias/input.tsx | 0 .../require-destructured-direct/expected.tsx | 0 .../require-destructured-direct/input.tsx | 0 .../require-destructured-multi/expected.tsx | 0 .../require-destructured-multi/input.tsx | 0 .../require-destructured-only/expected.tsx | 0 .../tests/require-destructured-only/input.tsx | 0 .../tests/require/expected.tsx | 0 .../tests/require/input.tsx | 0 .../tests/require/metrics.json | 0 .../tests/with-top-comment/expected.tsx | 0 .../tests/with-top-comment/input.tsx | 0 .../tsconfig.json | 0 .../workflow.yaml | 0 .../{ => jssg}/replace-act-import/README.md | 8 +- .../replace-act-import/codemod.yaml | 0 .../replace-act-import/package.json | 0 .../replace-act-import/scripts/codemod.ts | 0 .../tests/default-import/expected.tsx | 0 .../tests/default-import/input.tsx | 0 .../tests/default-import/metrics.json | 0 .../expected.tsx | 0 .../input.tsx | 0 .../metrics.json | 0 .../tests/multiple-named-imports/expected.tsx | 0 .../tests/multiple-named-imports/input.tsx | 0 .../tests/multiple-named-imports/metrics.json | 0 .../tests/named-import-2/expected.tsx | 0 .../tests/named-import-2/input.tsx | 0 .../tests/named-import-2/metrics.json | 0 .../tests/named-import/expected.tsx | 0 .../tests/named-import/input.tsx | 0 .../tests/named-import/metrics.json | 0 .../named-reexport-no-change/expected.tsx | 0 .../tests/named-reexport-no-change/input.tsx | 0 .../tests/other-import/expected.tsx | 0 .../tests/other-import/input.tsx | 0 .../tests/re-export/expected.tsx | 0 .../tests/re-export/input.tsx | 0 .../tests/re-export/metrics.json | 0 .../tests/wildcard-import-2/expected.tsx | 0 .../tests/wildcard-import-2/input.tsx | 0 .../tests/wildcard-import-2/metrics.json | 0 .../tests/wildcard-import/expected.tsx | 0 .../tests/wildcard-import/input.tsx | 0 .../tests/wildcard-import/metrics.json | 0 .../replace-act-import}/tsconfig.json | 0 .../replace-act-import/workflow.yaml | 0 .../replace-reactdom-render}/.gitignore | 0 .../jssg/replace-reactdom-render/README.md | 18 + .../replace-reactdom-render/codemod.yaml | 0 .../replace-reactdom-render/package.json | 0 .../replace-reactdom-render/pnpm-lock.yaml | 0 .../scripts/codemod.ts | 0 .../tests/default/expected.tsx | 0 .../tests/default/input.tsx | 0 .../tests/default/metrics.json | 0 .../expected.tsx | 0 .../multiple-member-render-imports/input.tsx | 0 .../metrics.json | 0 .../expected.tsx | 0 .../multiple-named-render-imports/input.tsx | 0 .../metrics.json | 0 .../tests/nested/expected.tsx | 0 .../tests/nested/input.tsx | 0 .../tests/nested/metrics.json | 0 .../replace-reactdom-render}/tsconfig.json | 0 .../replace-reactdom-render}/workflow.yaml | 0 .../replace-string-ref}/.gitignore | 0 .../{ => jssg}/replace-string-ref/README.md | 8 +- .../replace-string-ref/codemod.yaml | 0 .../replace-string-ref/package.json | 0 .../replace-string-ref/pnpm-lock.yaml | 0 .../replace-string-ref/scripts/codemod.ts | 0 .../expected.tsx | 0 .../input.tsx | 0 .../metrics.json | 0 .../expected.tsx | 0 .../class-component-default-import/input.tsx | 0 .../metrics.json | 0 .../class-component-named-import/expected.tsx | 0 .../class-component-named-import/input.tsx | 0 .../class-component-named-import/metrics.json | 0 .../tests/export-default-class/expected.tsx | 0 .../tests/export-default-class/input.tsx | 0 .../tests/export-default-class/metrics.json | 0 .../tests/function-component/expected.tsx | 0 .../tests/function-component/input.tsx | 0 .../expected.tsx | 0 .../input.tsx | 0 .../mixin-component-no-change/expected.tsx | 0 .../tests/mixin-component-no-change/input.tsx | 0 .../tests/multiple-string-refs/expected.tsx | 0 .../tests/multiple-string-refs/input.tsx | 0 .../tests/multiple-string-refs/metrics.json | 0 .../tests/namespace-import/expected.tsx | 0 .../tests/namespace-import/input.tsx | 0 .../tests/namespace-import/metrics.json | 0 .../non-identifier-ref-name/expected.tsx | 0 .../tests/non-identifier-ref-name/input.tsx | 0 .../non-identifier-ref-name/metrics.json | 0 .../replace-string-ref}/tsconfig.json | 0 .../replace-string-ref}/workflow.yaml | 0 .../jssg/replace-use-form-state/README.md | 18 + .../replace-use-form-state/codemod.yaml | 0 .../replace-use-form-state/package.json | 0 .../replace-use-form-state/scripts/codemod.ts | 0 .../tests/aliased-to-same-name/expected.tsx | 0 .../tests/aliased-to-same-name/input.tsx | 0 .../tests/aliased-to-same-name/metrics.json | 0 .../tests/default-and-named/expected.tsx | 0 .../tests/default-and-named/input.tsx | 0 .../tests/default-and-named/metrics.json | 0 .../tests/default-import/expected.tsx | 0 .../tests/default-import/input.tsx | 0 .../tests/default-import/metrics.json | 0 .../expected.tsx | 0 .../input.tsx | 0 .../expected.tsx | 0 .../input.tsx | 0 .../metrics.json | 0 .../expected.tsx | 0 .../input.tsx | 0 .../metrics.json | 0 .../tests/named-import-2/expected.tsx | 0 .../tests/named-import-2/input.tsx | 0 .../tests/named-import-2/metrics.json | 0 .../tests/named-import-3/expected.tsx | 0 .../tests/named-import-3/input.tsx | 0 .../tests/named-import-3/metrics.json | 0 .../tests/named-import/expected.tsx | 0 .../tests/named-import/input.tsx | 0 .../tests/named-import/metrics.json | 0 .../tests/namespace-and-named/expected.tsx | 0 .../tests/namespace-and-named/input.tsx | 0 .../tests/namespace-and-named/metrics.json | 0 .../tests/other-import/expected.tsx | 0 .../tests/other-import/input.tsx | 0 .../tests/shadowed-local-binding/expected.tsx | 0 .../tests/shadowed-local-binding/input.tsx | 0 .../tests/shadowed-local-binding/metrics.json | 0 .../tests/type-identifier-rename/expected.tsx | 0 .../tests/type-identifier-rename/input.tsx | 0 .../tests/type-identifier-rename/metrics.json | 0 .../tests/wildcard-import/expected.tsx | 0 .../tests/wildcard-import/input.tsx | 0 .../tests/wildcard-import/metrics.json | 0 .../replace-use-form-state}/tsconfig.json | 0 .../replace-use-form-state/workflow.yaml | 0 .../use-context-hook}/.gitignore | 0 codemods/jssg/use-context-hook/README.md | 18 + .../{ => jssg}/use-context-hook/codemod.yaml | 0 .../{ => jssg}/use-context-hook/package.json | 0 .../use-context-hook}/pnpm-lock.yaml | 0 .../use-context-hook/scripts/codemod.ts | 0 .../tests/aliased-use-context/expected.tsx | 0 .../tests/aliased-use-context/input.tsx | 0 .../tests/aliased-use-context/metrics.json | 0 .../tests/any-use-context/expected.tsx | 0 .../tests/any-use-context/input.tsx | 0 .../tests/mixed-import/expected.tsx | 0 .../tests/mixed-import/input.tsx | 0 .../tests/mixed-import/metrics.json | 0 .../multiple-use-context-imports/expected.tsx | 0 .../multiple-use-context-imports/input.tsx | 0 .../multiple-use-context-imports/metrics.json | 0 .../tests/shadowed-local-binding/expected.tsx | 0 .../tests/shadowed-local-binding/input.tsx | 0 .../tests/shadowed-local-binding/metrics.json | 0 .../tests/type-identifier-rename/expected.tsx | 0 .../tests/type-identifier-rename/input.tsx | 0 .../tests/type-identifier-rename/metrics.json | 0 .../typescript-any-use-context/expected.tsx | 0 .../typescript-any-use-context/input.tsx | 0 .../typescript-use-context-2/expected.tsx | 0 .../tests/typescript-use-context-2/input.tsx | 0 .../typescript-use-context-2/metrics.json | 0 .../tests/typescript-use-context/expected.tsx | 0 .../tests/typescript-use-context/input.tsx | 0 .../tests/typescript-use-context/metrics.json | 0 .../tests/use-context-2/expected.tsx | 0 .../tests/use-context-2/input.tsx | 0 .../tests/use-context-2/metrics.json | 0 .../tests/use-context/expected.tsx | 0 .../tests/use-context/input.tsx | 0 .../tests/use-context/metrics.json | 0 .../use-context-hook}/tsconfig.json | 0 .../use-context-hook}/workflow.yaml | 0 codemods/legacy/.babelrc | 4 + codemods/legacy/jest/require-actual.d.ts | 5 + codemods/legacy/jest/setup-require-actual.js | 6 + codemods/legacy/package.json | 42 + codemods/legacy/pnpm-lock.yaml | 6904 +++++++++++++++++ .../React-DOM-to-react-dom-factories.js | 177 + .../transforms/ReactNative-View-propTypes.js | 218 + .../transforms/__testfixtures__/.eslintrc | 5 + .../react-dom-basic-case.input.js} | 0 .../react-dom-basic-case.output.js} | 0 .../react-dom-deconstructed-import.input.js} | 0 .../react-dom-deconstructed-import.output.js} | 0 ...m-deconstructed-require-part-two.input.js} | 0 ...-deconstructed-require-part-two.output.js} | 0 .../react-dom-deconstructed-require.input.js} | 0 ...react-dom-deconstructed-require.output.js} | 0 ...-import-dom-from-other-libraries.input.js} | 0 ...-import-dom-from-other-libraries.output.js | 0 .../react-dom-no-change-import.input.js} | 0 .../react-dom-no-change-import.output.js | 0 ...e-local-dom-from-other-libraries.input.js} | 0 ...e-local-dom-from-other-libraries.output.js | 0 ...require-dom-from-other-libraries.input.js} | 0 ...require-dom-from-other-libraries.output.js | 0 .../react-dom-no-change-require.input.js} | 0 .../react-dom-no-change-require.output.js | 0 .../default-import-multi-reference.input.js} | 0 .../default-import-multi-reference.output.js} | 0 .../default-import-only-reference.input.js} | 0 .../default-import-only-reference.output.js} | 0 .../default-require-multi-reference.input.js} | 0 ...default-require-multi-reference.output.js} | 0 .../default-require-only-reference.input.js} | 0 .../default-require-only-reference.output.js} | 0 ...tructured-import-multi-reference.input.js} | 0 ...ructured-import-multi-reference.output.js} | 0 ...structured-import-only-reference.input.js} | 0 ...tructured-import-only-reference.output.js} | 0 ...ructured-require-multi-reference.input.js} | 0 ...uctured-require-multi-reference.output.js} | 0 ...tructured-require-only-reference.input.js} | 0 ...ructured-require-only-reference.output.js} | 0 .../import-flow-type-with-require.input.js} | 0 .../import-flow-type-with-require.output.js} | 0 .../multiple-replacements.input.js} | 0 .../multiple-replacements.output.js} | 0 .../noop-import.input.js} | 0 .../noop-import.output.js | 0 .../noop-require.input.js} | 0 .../noop-require.output.js | 0 .../class/class-anonymous.input.js | 21 + .../class/class-anonymous.output.js | 21 + .../class/class-anonymous2.input.js | 50 + .../class/class-anonymous2.output.js | 50 + .../class/class-create-class-naming.input.js | 12 + .../class/class-create-class-naming.output.js | 16 + .../class/class-displayName.input.js | 31 + .../class/class-displayName.output.js | 41 + .../class/class-flow1.input.js | 40 + .../class/class-flow1.output.js | 65 + .../class/class-flow2.input.js | 52 + .../class/class-flow2.output.js | 54 + .../class/class-flow3.input.js | 47 + .../class/class-flow3.output.js | 64 + .../class/class-flow4.input.js | 44 + .../class/class-flow4.output.js | 44 + .../class/class-flow5.input.js | 40 + .../class/class-flow5.output.js | 39 + .../class/class-flow6.input.js | 41 + .../class/class-flow6.output.js | 62 + .../class/class-flow7.input.js | 41 + .../class/class-flow7.output.js | 37 + .../class/class-initial-state.input.js | 335 + .../class/class-initial-state.output.js | 362 + .../class/class-no-conversion.input.js | 9 + .../class/class-no-conversion.output.js | 13 + .../class/class-no-display-name.input.js | 10 + .../class/class-no-display-name.output.js | 12 + .../class/class-property-field.input.js | 28 + .../class/class-property-field.output.js | 26 + .../class/class-prune-react.input.js | 16 + .../class/class-prune-react.output.js | 18 + .../class/class-prune-react2.input.js | 16 + .../class/class-prune-react2.output.js | 20 + .../class/class-prune-react3.input.js | 19 + .../class/class-prune-react3.output.js | 24 + .../class/class-prune-react4.input.js | 19 + .../class/class-prune-react4.output.js | 24 + .../class/class-pure-mixin1.input.js | 19 + .../class/class-pure-mixin1.output.js | 14 + .../class/class-pure-mixin2.input.js | 29 + .../class/class-pure-mixin2.output.js | 24 + .../class/class-pure-mixin3.input.js | 20 + .../class/class-pure-mixin3.output.js | 22 + .../class/class-pure-mixin4.input.js | 36 + .../class/class-pure-mixin4.output.js | 32 + .../class/class-pure-mixin5.input.js | 19 + .../class/class-pure-mixin5.output.js | 14 + .../class/class-test2.input.js | 116 + .../class/class-test2.output.js | 115 + .../class/class-top-comment.input.js | 27 + .../class/class-top-comment.output.js | 22 + .../__testfixtures__/class/class.input.js | 214 + .../__testfixtures__/class/class.output.js | 225 + .../class/export-default-class.input.js | 21 + .../class/export-default-class.output.js | 19 + ...t-to-jsx-allow-member-expression.input.js} | 0 ...-to-jsx-allow-member-expression.output.js} | 0 ...create-element-to-jsx-arg-spread.input.js} | 0 ...reate-element-to-jsx-arg-spread.output.js} | 0 ...-element-to-jsx-call-as-children.input.js} | 0 ...element-to-jsx-call-as-children.output.js} | 0 ...t-to-jsx-call-expression-as-prop.input.js} | 0 ...-to-jsx-call-expression-as-prop.output.js} | 0 ...-element-to-jsx-children-literal.input.js} | 0 ...element-to-jsx-children-literal.output.js} | 0 ...eate-element-to-jsx-children-map.input.js} | 0 ...ate-element-to-jsx-children-map.output.js} | 0 ...-jsx-children-mixed-empty-string.input.js} | 0 ...jsx-children-mixed-empty-string.output.js} | 0 .../create-element-to-jsx-children.input.js} | 0 .../create-element-to-jsx-children.output.js} | 0 ...lement-to-jsx-computed-component.input.js} | 0 ...ement-to-jsx-computed-component.output.js} | 0 ...eate-element-to-jsx-deep-nesting.input.js} | 0 ...ate-element-to-jsx-deep-nesting.output.js} | 0 ...-jsx-element-comment-positioning.input.js} | 0 ...jsx-element-comment-positioning.output.js} | 0 ...te-element-to-jsx-escaped-string.input.js} | 0 ...e-element-to-jsx-escaped-string.output.js} | 0 ...te-element-to-jsx-gt-lt-entities.input.js} | 0 ...e-element-to-jsx-gt-lt-entities.output.js} | 0 ...to-jsx-ignore-bad-capitalization.input.js} | 0 ...o-jsx-ignore-bad-capitalization.output.js} | 0 ...eate-element-to-jsx-literal-prop.input.js} | 0 ...ate-element-to-jsx-literal-prop.output.js} | 0 ...e-element-to-jsx-literal-spacing.input.js} | 0 ...-element-to-jsx-literal-spacing.output.js} | 0 ...to-jsx-member-expression-as-prop.input.js} | 0 ...o-jsx-member-expression-as-prop.output.js} | 0 ...eate-element-to-jsx-no-props-arg.input.js} | 0 ...ate-element-to-jsx-no-props-arg.output.js} | 0 .../create-element-to-jsx-no-react.input.js} | 0 .../create-element-to-jsx-no-react.output.js | 0 ...ate-element-to-jsx-object-assign.input.js} | 0 ...te-element-to-jsx-object-assign.output.js} | 0 ...element-to-jsx-preserve-comments.input.js} | 0 ...lement-to-jsx-preserve-comments.output.js} | 0 ...reate-element-to-jsx-props-array.input.js} | 0 ...eate-element-to-jsx-props-array.output.js} | 0 ...ate-element-to-jsx-props-boolean.input.js} | 0 ...te-element-to-jsx-props-boolean.output.js} | 0 .../create-element-to-jsx-props.input.js} | 0 .../create-element-to-jsx-props.output.js} | 0 ...eate-element-to-jsx-react-spread.input.js} | 0 ...ate-element-to-jsx-react-spread.output.js} | 0 ...te-element-to-jsx-single-element.input.js} | 0 ...e-element-to-jsx-single-element.output.js} | 0 ...eate-element-to-jsx-spread-props.input.js} | 0 ...ate-element-to-jsx-spread-props.output.js} | 0 .../create-element-to-jsx-spread.input.js} | 0 .../create-element-to-jsx-spread.output.js} | 0 .../custom-sort-group/.eslintrc | 0 .../custom-sort-group.input.js} | 0 .../custom-sort-group.output.js} | 0 .../__testfixtures__}/custom-sort/.eslintrc | 0 .../custom-sort/custom-sort.input.js} | 0 .../custom-sort/custom-sort.output.js} | 0 .../class-component.input.js} | 0 .../class-component.output.js} | 0 .../create-class-component.input.js} | 0 .../create-class-component.output.js} | 0 .../__testfixtures__/findDOMNode.input.js} | 0 .../__testfixtures__/findDOMNode.output.js} | 0 .../manual-bind-to-arrow1.input.js} | 0 .../manual-bind-to-arrow1.output.js} | 0 .../manual-bind-to-arrow10.input.js} | 0 .../manual-bind-to-arrow10.output.js} | 2 +- .../manual-bind-to-arrow11.input.js} | 0 .../manual-bind-to-arrow11.output.js | 0 .../manual-bind-to-arrow2.input.js} | 0 .../manual-bind-to-arrow2.output.js} | 0 .../manual-bind-to-arrow3.input.js} | 0 .../manual-bind-to-arrow3.output.js | 0 .../manual-bind-to-arrow4.input.js} | 0 .../manual-bind-to-arrow4.output.js} | 0 .../manual-bind-to-arrow5.input.js} | 0 .../manual-bind-to-arrow5.output.js} | 0 .../manual-bind-to-arrow6.input.js} | 0 .../manual-bind-to-arrow6.output.js} | 2 +- .../manual-bind-to-arrow7.input.js} | 0 .../manual-bind-to-arrow7.output.js} | 2 +- .../manual-bind-to-arrow8.input.js} | 0 .../manual-bind-to-arrow8.output.js} | 2 +- .../manual-bind-to-arrow9.input.js} | 0 .../manual-bind-to-arrow9.output.js} | 2 +- .../pure-component-destructuring.input.js} | 0 .../pure-component-destructuring.output.js} | 0 .../__testfixtures__/pure-component.input.js} | 0 .../pure-component.output.js} | 0 .../pure-component2.input.js} | 0 .../pure-component2.output.js} | 0 .../pure-render-mixin.input.js} | 0 .../pure-render-mixin.output.js} | 0 .../pure-render-mixin2.input.js} | 0 .../pure-render-mixin2.output.js} | 0 .../pure-render-mixin3.input.js} | 0 .../pure-render-mixin3.output.js} | 0 .../pure-render-mixin4.input.js} | 0 .../pure-render-mixin4.output.js} | 0 .../import-dom-base.input.js} | 0 .../import-dom-base.output.js} | 0 .../import-multiple-specifiers.input.js} | 0 .../import-multiple-specifiers.output.js} | 0 .../import-server-base.input.js} | 0 .../import-server-base.output.js} | 0 .../import-with-existing-react-dom.input.js} | 0 .../import-with-existing-react-dom.output.js} | 0 ...import-without-default-specifier.input.js} | 0 ...mport-without-default-specifier.output.js} | 0 .../mixed-with-existing-react-dom.input.js} | 0 .../mixed-with-existing-react-dom.output.js} | 0 .../require-dom-base.input.js} | 0 .../require-dom-base.output.js} | 0 .../require-indirect.input.js} | 0 .../require-indirect.output.js} | 0 .../require-keeps-react.input.js} | 0 .../require-keeps-react.output.js} | 0 .../require-server-base.input.js} | 0 .../require-server-base.output.js} | 0 .../no-provider.input.js} | 0 .../no-provider.output.js | 0 .../typescript/no-provider.input.js} | 0 .../typescript/no-provider.output.js | 0 .../typescript/with-provider-2.input.js} | 0 .../typescript/with-provider-2.output.js} | 0 .../typescript/with-provider.input.js} | 0 .../typescript/with-provider.output.js} | 0 .../with-provider-2.input.js} | 0 .../with-provider-2.output.js} | 0 .../with-provider.input.js} | 0 .../with-provider.output.js} | 0 .../arrow-function-expression.input.js} | 0 .../arrow-function-expression.output.js} | 0 .../callee-is-member-expression.input.js} | 0 .../callee-is-member-expression.output.js} | 0 .../forward-ref-import-2.input.js} | 0 .../forward-ref-import-2.output.js} | 0 .../forward-ref-import.input.js} | 0 .../forward-ref-import.output.js} | 0 .../function-expression.input.js} | 0 .../function-expression.output.js} | 0 .../props-identifier.input.js} | 0 .../props-identifier.output.js} | 0 .../props-object-pattern.input.js} | 0 .../props-object-pattern.output.js} | 0 .../typescript/props-type-literal.input.js} | 0 .../typescript/props-type-literal.output.js} | 0 .../type-arguments-custom-names.input.js} | 0 .../type-arguments-custom-names.output.js} | 0 .../type-arguments-type-literals.input.js} | 0 .../type-arguments-type-literals.output.js} | 0 .../typescript/type-arguments.input.js} | 0 .../typescript/type-arguments.output.js} | 0 .../arrow-functions.input.js} | 0 .../arrow-functions.output.js} | 0 .../create-react-class.input.js} | 0 .../create-react-class.output.js} | 0 .../instance-methods.input.js} | 0 .../instance-methods.output.js} | 0 .../manually-calling-lifecycles.input.js} | 0 .../manually-calling-lifecycles.output.js} | 0 .../manually-invoked-mixin-methods.input.js} | 0 .../manually-invoked-mixin-methods.output.js} | 0 .../one-lifecycle-calls-another.input.js} | 0 .../one-lifecycle-calls-another.output.js} | 0 .../standalone-function.input.js} | 0 .../standalone-function.output.js | 0 .../typescript/class.tsx.input.js} | 0 .../typescript/class.tsx.output.js} | 0 .../variable-within-class-method.input.js} | 0 .../variable-within-class-method.output.js | 0 .../__testfixtures__/sort-comp-pure.input.js} | 0 .../sort-comp-pure.output.js} | 0 .../__testfixtures__/sort-comp.input.js} | 0 .../__testfixtures__/sort-comp.output.js} | 0 .../__testfixtures__/sort-comp2.input.js} | 0 .../__testfixtures__/sort-comp2.output.js} | 0 .../__testfixtures__/sort-comp3.input.js} | 0 .../__testfixtures__/sort-comp3.output.js} | 0 ...specifiers-import-react-variable.input.js} | 0 ...pecifiers-import-react-variable.output.js} | 1 + ...t-and-multiple-specifiers-import.input.js} | 0 ...-and-multiple-specifiers-import.output.js} | 1 + ...-named-imports-react-not-removed.input.js} | 0 ...-named-imports-react-not-removed.output.js | 0 ...ture-named-imports-variable-used.input.js} | 0 ...ture-named-imports-variable-used.output.js | 0 .../destructure-named-imports.input.js} | 0 .../destructure-named-imports.output.js | 5 + ...-specifier-import-react-variable.input.js} | 0 ...specifier-import-react-variable.output.js} | 1 + ...efault-and-type-specifier-import.input.js} | 0 ...fault-and-type-specifier-import.output.js} | 1 + .../jsx-element.input.js} | 0 .../jsx-element.output.js} | 0 .../jsx-fragment.input.js} | 0 .../jsx-fragment.output.js} | 0 .../leading-comment.input.js} | 0 .../leading-comment.output.js} | 0 .../react-already-used-named-export.input.js} | 0 .../react-already-used-named-export.output.js | 0 ...xport-jsx-element-react-variable.input.js} | 0 ...port-jsx-element-react-variable.output.js} | 1 + ...basic-default-export-jsx-element.input.js} | 0 ...asic-default-export-jsx-element.output.js} | 0 .../react-basic-default-export.input.js} | 0 .../react-basic-default-export.output.js} | 1 + .../react-jsx-member-expression.input.js} | 0 .../react-jsx-member-expression.output.js} | 3 +- .../react-not-removed.input.js} | 0 .../react-not-removed.output.js} | 1 + .../react-type-default-export.input.js} | 0 .../react-type-default-export.output.js} | 1 + .../react-type-not-removed.input.js} | 0 .../react-type-not-removed.output.js} | 1 + ...ifiers-import-react-variable.tsx.input.js} | 0 ...fiers-import-react-variable.tsx.output.js} | 1 + ...d-multiple-specifiers-import.tsx.input.js} | 0 ...-multiple-specifiers-import.tsx.output.js} | 1 + .../typescript/jsx-element.tsx.input.js} | 0 .../typescript/jsx-element.tsx.output.js} | 0 .../typescript/jsx-fragment.tsx.input.js} | 0 .../typescript/jsx-fragment.tsx.output.js} | 0 .../typescript/leading-comment.tsx.input.js} | 0 .../typescript/leading-comment.tsx.output.js} | 0 .../preserve-types-default.tsx.input.js} | 0 .../preserve-types-default.tsx.output.js} | 0 .../preserve-types-namespace.tsx.input.js} | 1 + .../preserve-types-namespace.tsx.output.js | 0 ...ct-already-used-named-export.tsx.input.js} | 0 ...ct-already-used-named-export.tsx.output.js | 0 ...t-jsx-element-react-variable.tsx.input.js} | 0 ...-jsx-element-react-variable.tsx.output.js} | 1 + ...c-default-export-jsx-element.tsx.input.js} | 0 ...-default-export-jsx-element.tsx.output.js} | 0 .../react-basic-default-export.tsx.input.js} | 0 .../react-basic-default-export.tsx.output.js} | 1 + .../react-jsx-member-expression.tsx.input.js} | 0 ...react-jsx-member-expression.tsx.output.js} | 3 +- .../react-not-removed.tsx.input.js} | 0 .../react-not-removed.tsx.output.js} | 3 +- .../variable-already-used.tsx.input.js} | 0 .../variable-already-used.tsx.output.js | 0 .../variable-already-used.input.js} | 0 .../variable-already-used.output.js | 0 .../React-DOM-to-react-dom-factories-test.js | 34 + .../ReactNative-View-propTypes-test.js | 43 + .../legacy/transforms/__tests__/class-test.js | 68 + .../__tests__/create-element-to-jsx-test.js | 199 + .../transforms/__tests__/custom-sort-group.js | 14 + .../transforms/__tests__/custom-sort.js | 14 + .../transforms/__tests__/error-boundaries.js | 17 + .../transforms/__tests__/findDOMNode-test.js | 12 + .../__tests__/manual-bind-to-arrow-test.js | 40 + .../__tests__/pure-component-test.js | 20 + .../__tests__/pure-render-mixin-test.js | 20 + .../__tests__/react-to-react-dom-test.js | 34 + .../__tests__/remove-context-provider.test.ts | 51 + .../__tests__/remove-forward-ref.test.ts | 62 + .../rename-unsafe-lifecycles-test.js | 74 + .../transforms/__tests__/sort-comp-test.js | 15 + .../__tests__/update-react-imports-test.js | 94 + codemods/legacy/transforms/class.js | 1426 ++++ .../transforms/create-element-to-jsx.js | 269 + .../legacy/transforms/error-boundaries.js | 12 + codemods/legacy/transforms/findDOMNode.js | 151 + .../legacy/transforms/manual-bind-to-arrow.js | 184 + codemods/legacy/transforms/pure-component.js | 339 + .../legacy/transforms/pure-render-mixin.js | 179 + .../legacy/transforms/react-to-react-dom.js | 421 + .../transforms/remove-context-provider.ts | 40 + .../legacy/transforms/remove-forward-ref.ts | 297 + .../transforms/rename-unsafe-lifecycles.js | 62 + codemods/legacy/transforms/sort-comp.js | 239 + .../legacy/transforms/update-react-imports.js | 380 + .../legacy/transforms/utils/ReactUtils.js | 308 + .../transforms/utils/array-polyfills.js | 46 + .../transforms/utils/doesNotUseArguments.js | 40 + codemods/legacy/tsconfig.json | 6 + codemods/manual-bind-to-arrow/README.md | 16 - codemods/manual-bind-to-arrow/codemod.yaml | 19 - codemods/manual-bind-to-arrow/package.json | 15 - .../manual-bind-to-arrow/scripts/codemod.ts | 188 - .../tests/manual-bind-to-arrow1/metrics.json | 10 - .../tests/manual-bind-to-arrow10/metrics.json | 10 - .../tests/manual-bind-to-arrow11/input.tsx | 12 - .../tests/manual-bind-to-arrow2/metrics.json | 10 - .../tests/manual-bind-to-arrow3/input.tsx | 7 - .../tests/manual-bind-to-arrow4/metrics.json | 10 - .../tests/manual-bind-to-arrow5/metrics.json | 10 - .../tests/manual-bind-to-arrow6/metrics.json | 10 - .../tests/manual-bind-to-arrow7/metrics.json | 10 - .../tests/manual-bind-to-arrow8/metrics.json | 10 - .../tests/manual-bind-to-arrow9/metrics.json | 10 - codemods/manual-bind-to-arrow/tsconfig.json | 22 - codemods/manual-bind-to-arrow/workflow.yaml | 13 - codemods/pure-component/README.md | 16 - codemods/pure-component/codemod.yaml | 19 - codemods/pure-component/package.json | 15 - codemods/pure-component/scripts/codemod.ts | 583 -- .../scripts/differential-tests.mjs | 80 - .../pure-component/scripts/warning-tests.mjs | 83 - .../pure-component/tests/arrow/metrics.json | 10 - .../tests/arrow/test.config.json | 5 - .../pure-component/tests/default/metrics.json | 10 - .../tests/destructuring/metrics.json | 10 - .../tests/destructuring/test.config.json | 5 - .../tests/export-default-arrow/expected.tsx | 4 - .../tests/export-default-arrow/input.tsx | 5 - .../tests/export-default-arrow/metrics.json | 10 - .../export-default-arrow/test.config.json | 5 - .../export-default-function/expected.tsx | 3 - .../tests/export-default-function/input.tsx | 5 - .../export-default-function/metrics.json | 10 - .../tests/mixed-superclasses/expected.tsx | 7 - .../tests/mixed-superclasses/input.tsx | 9 - .../tests/mixed-superclasses/metrics.json | 10 - .../tests/named-export-arrow/expected.tsx | 3 - .../tests/named-export-arrow/input.tsx | 5 - .../tests/named-export-arrow/metrics.json | 10 - .../tests/named-export-arrow/test.config.json | 5 - .../tests/named-export/expected.tsx | 3 - .../tests/named-export/input.tsx | 5 - .../tests/named-export/metrics.json | 10 - .../tests/named-import-component/expected.tsx | 3 - .../tests/named-import-component/input.tsx | 5 - .../tests/named-import-component/metrics.json | 10 - .../expected.tsx | 3 - .../input.tsx | 5 - .../metrics.json | 10 - .../react-native-named-import/expected.tsx | 3 - .../tests/react-native-named-import/input.tsx | 5 - .../react-native-named-import/metrics.json | 10 - codemods/pure-component/workflow.yaml | 13 - codemods/pure-render-mixin/README.md | 21 - codemods/pure-render-mixin/codemod.yaml | 19 - codemods/pure-render-mixin/package.json | 15 - codemods/pure-render-mixin/scripts/codemod.ts | 244 - .../expected.tsx | 11 - .../input.tsx | 11 - .../no-react-explicit-require/expected.tsx | 10 - .../tests/no-react-explicit-require/input.tsx | 9 - .../no-react-explicit-require/metrics.json | 10 - .../test.config.json | 5 - .../tests/no-react-no-change/expected.tsx | 9 - .../tests/no-react-no-change/input.tsx | 9 - .../tests/pure-render-mixin/metrics.json | 10 - .../tests/pure-render-mixin2/metrics.json | 10 - .../tests/pure-render-mixin3/metrics.json | 10 - .../tests/pure-render-mixin4/metrics.json | 10 - .../tests/pure-render-mixin4/test.config.json | 5 - codemods/pure-render-mixin/workflow.yaml | 21 - .../README.md | 16 - .../codemod.yaml | 19 - .../package.json | 18 - .../scripts/codemod.ts | 173 - .../tests/react-dom-basic-case/metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../input.tsx | 4 - .../react-dom-no-change-import/input.tsx | 7 - .../input.tsx | 7 - .../expected.tsx | 8 - .../input.tsx | 8 - .../input.tsx | 4 - .../react-dom-no-change-require/input.tsx | 7 - .../workflow.yaml | 13 - .../react-native-view-prop-types/README.md | 16 - .../react-native-view-prop-types/codemod.yaml | 19 - .../react-native-view-prop-types/package.json | 18 - .../scripts/codemod.ts | 263 - .../metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../tests/multiple-replacements/metrics.json | 10 - .../tests/noop-import/input.tsx | 6 - .../tests/noop-require/input.tsx | 6 - .../tsconfig.json | 27 - .../workflow.yaml | 13 - .../react-proptypes-to-prop-types/README.md | 20 - .../tsconfig.json | 27 - codemods/react-to-react-dom/README.md | 16 - codemods/react-to-react-dom/codemod.yaml | 19 - codemods/react-to-react-dom/package.json | 15 - .../react-to-react-dom/scripts/codemod.ts | 713 -- .../scripts/error-tests.mjs | 82 - .../import-dom-base-uppercase/expected.tsx | 4 - .../tests/import-dom-base-uppercase/input.tsx | 4 - .../import-dom-base-uppercase/metrics.json | 12 - .../tests/import-dom-base/metrics.json | 12 - .../import-dom-type-identifier/expected.tsx | 5 - .../import-dom-type-identifier/input.tsx | 5 - .../import-dom-type-identifier/metrics.json | 12 - .../import-multiple-specifiers/metrics.json | 12 - .../tests/import-server-base/metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../require-dom-base-uppercase/expected.tsx | 4 - .../require-dom-base-uppercase/input.tsx | 4 - .../require-dom-base-uppercase/metrics.json | 12 - .../tests/require-dom-base/metrics.json | 12 - .../tests/require-indirect-mixed/expected.tsx | 12 - .../tests/require-indirect-mixed/input.tsx | 8 - .../tests/require-indirect-mixed/metrics.json | 12 - .../tests/require-indirect/metrics.json | 12 - .../tests/require-keeps-react/metrics.json | 12 - .../tests/require-server-base/metrics.json | 12 - codemods/react-to-react-dom/tsconfig.json | 27 - codemods/react-to-react-dom/workflow.yaml | 13 - codemods/remove-context-provider/README.md | 16 - codemods/remove-context-provider/codemod.yaml | 19 - codemods/remove-context-provider/package.json | 15 - .../scripts/codemod.ts | 58 - .../nested-member-no-change/expected.tsx | 7 - .../tests/nested-member-no-change/input.tsx | 7 - .../typescript-with-provider-2/expected.tsx | 9 - .../typescript-with-provider-2/metrics.json | 10 - .../typescript-with-provider/metrics.json | 10 - .../tests/with-provider-2/expected.tsx | 9 - .../tests/with-provider-2/metrics.json | 10 - .../tests/with-provider/metrics.json | 10 - .../remove-context-provider/tsconfig.json | 27 - .../remove-context-provider/workflow.yaml | 13 - codemods/remove-forward-ref/README.md | 16 - codemods/remove-forward-ref/codemod.yaml | 19 - codemods/remove-forward-ref/package.json | 18 - .../remove-forward-ref/scripts/codemod.ts | 279 - .../arrow-function-expression/metrics.json | 10 - .../callee-is-member-expression/metrics.json | 10 - .../tests/forward-ref-import-2/metrics.json | 10 - .../tests/forward-ref-import/metrics.json | 10 - .../tests/function-expression/metrics.json | 10 - .../expected.tsx | 5 - .../invalid-props-pattern-no-change/input.tsx | 5 - .../invalid-render-arg-no-change/expected.tsx | 3 - .../invalid-render-arg-no-change/input.tsx | 3 - .../tests/multiple-react-imports/expected.tsx | 10 - .../tests/multiple-react-imports/input.tsx | 6 - .../tests/multiple-react-imports/metrics.json | 10 - .../tests/props-identifier/metrics.json | 10 - .../tests/props-object-pattern/metrics.json | 10 - .../metrics.json | 10 - .../typescript-qualified-names/expected.tsx | 10 - .../typescript-qualified-names/input.tsx | 8 - .../typescript-qualified-names/metrics.json | 10 - .../metrics.json | 10 - .../metrics.json | 10 - .../typescript-type-arguments/metrics.json | 10 - .../expected.tsx | 11 - .../input.tsx | 11 - .../metrics.json | 10 - .../expected.tsx | 11 - .../input.tsx | 8 - .../metrics.json | 10 - codemods/remove-forward-ref/tsconfig.json | 27 - codemods/remove-forward-ref/workflow.yaml | 13 - codemods/rename-unsafe-lifecycles/README.md | 16 - .../rename-unsafe-lifecycles/codemod.yaml | 20 - .../rename-unsafe-lifecycles/package.json | 15 - .../rename-unsafe-lifecycles/pnpm-lock.yaml | 32 - .../scripts/codemod.ts | 130 - .../tests/arrow-functions/metrics.json | 11 - .../tests/create-react-class/metrics.json | 11 - .../tests/instance-methods/metrics.json | 11 - .../manually-calling-lifecycles/metrics.json | 11 - .../metrics.json | 11 - .../one-lifecycle-calls-another/metrics.json | 11 - .../tests/standalone-function/input.tsx | 9 - .../tests/typescript-class-tsx/metrics.json | 11 - .../variable-within-class-method/input.tsx | 7 - .../rename-unsafe-lifecycles/tsconfig.json | 27 - .../rename-unsafe-lifecycles/workflow.yaml | 13 - codemods/replace-act-import/tsconfig.json | 27 - codemods/replace-reactdom-render/README.md | 16 - .../replace-reactdom-render/tsconfig.json | 27 - .../replace-reactdom-render/workflow.yaml | 13 - codemods/replace-string-ref/tsconfig.json | 27 - codemods/replace-string-ref/workflow.yaml | 13 - codemods/replace-use-form-state/README.md | 16 - codemods/replace-use-form-state/tsconfig.json | 27 - codemods/sort-comp/README.md | 23 - codemods/sort-comp/codemod.yaml | 20 - codemods/sort-comp/package.json | 15 - codemods/sort-comp/scripts/codemod.ts | 626 -- .../tests/custom-sort-group/metrics.json | 11 - .../sort-comp/tests/custom-sort/metrics.json | 11 - .../expected.tsx | 9 - .../input.tsx | 9 - .../tests/methods-order-option/expected.tsx | 11 - .../tests/methods-order-option/input.tsx | 11 - .../tests/methods-order-option/metrics.json | 11 - .../methods-order-option/test.config.json | 5 - .../no-react-explicit-require/expected.tsx | 7 - .../tests/no-react-explicit-require/input.tsx | 7 - .../no-react-explicit-require/metrics.json | 11 - .../test.config.json | 5 - .../tests/no-react-no-change/expected.tsx | 7 - .../tests/no-react-no-change/input.tsx | 7 - .../tests/sort-comp-pure/metrics.json | 11 - .../sort-comp/tests/sort-comp/metrics.json | 11 - .../sort-comp/tests/sort-comp2/metrics.json | 11 - .../sort-comp/tests/sort-comp3/metrics.json | 11 - .../expected.tsx | 9 - .../unrelated-component-no-change/input.tsx | 9 - codemods/sort-comp/tsconfig.json | 24 - codemods/sort-comp/workflow.yaml | 22 - codemods/update-react-imports/.gitignore | 33 - codemods/update-react-imports/README.md | 20 - codemods/update-react-imports/codemod.yaml | 20 - codemods/update-react-imports/package.json | 18 - .../update-react-imports/scripts/codemod.ts | 250 - .../metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../test.config.json | 5 - .../input.tsx | 6 - .../metrics.json | 12 - .../test.config.json | 5 - .../destructure-named-imports/expected.tsx | 4 - .../destructure-named-imports/metrics.json | 12 - .../test.config.json | 5 - .../metrics.json | 12 - .../metrics.json | 12 - .../tests/jsx-element/metrics.json | 12 - .../tests/jsx-fragment/metrics.json | 12 - .../tests/leading-comment/metrics.json | 12 - .../expected.tsx | 2 - .../metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../react-basic-default-export/metrics.json | 12 - .../react-jsx-member-expression/metrics.json | 12 - .../tests/react-not-removed/expected.tsx | 6 - .../tests/react-not-removed/metrics.json | 12 - .../react-type-default-export/metrics.json | 12 - .../tests/react-type-not-removed/metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../typescript-jsx-element-tsx/metrics.json | 12 - .../typescript-jsx-fragment-tsx/metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../expected.tsx | 4 - .../metrics.json | 12 - .../expected.tsx | 2 - .../metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../metrics.json | 12 - .../expected.tsx | 6 - .../metrics.json | 12 - .../tests/variable-already-used/expected.tsx | 6 - .../tests/variable-already-used/metrics.json | 12 - codemods/update-react-imports/tsconfig.json | 27 - codemods/update-react-imports/workflow.yaml | 21 - codemods/use-context-hook/.gitignore | 33 - codemods/use-context-hook/README.md | 16 - codemods/use-context-hook/pnpm-lock.yaml | 41 - codemods/use-context-hook/tsconfig.json | 27 - codemods/use-context-hook/workflow.yaml | 13 - package.json | 33 +- pnpm-lock.yaml | 200 +- pnpm-workspace.yaml | 2 +- scripts/validate-docs.mjs | 33 + 1011 files changed, 15747 insertions(+), 9588 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 LEGACY.md create mode 100644 PARITY_STATUS.md delete mode 100644 codemods/create-element-to-jsx/README.md delete mode 100644 codemods/create-element-to-jsx/codemod.yaml delete mode 100644 codemods/create-element-to-jsx/package.json delete mode 100644 codemods/create-element-to-jsx/scripts/codemod.ts delete mode 100644 codemods/create-element-to-jsx/scripts/differential-tests.mjs delete mode 100644 codemods/create-element-to-jsx/scripts/error-tests.mjs delete mode 100644 codemods/create-element-to-jsx/tests/allow-member-expression/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/arg-spread/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/call-as-children/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/call-expression-as-prop/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/children-literal/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/children-map/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/children-mixed-empty-string/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/children/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/comment-only-props-string-child/expected.tsx delete mode 100644 codemods/create-element-to-jsx/tests/comment-only-props-string-child/input.tsx delete mode 100644 codemods/create-element-to-jsx/tests/comment-only-props-string-child/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/deep-nesting/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/element-comment-positioning/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/escaped-string/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/gt-lt-entities/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/ignore-bad-capitalization/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/literal-prop/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/literal-spacing/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/member-child-comments-alt/expected.tsx delete mode 100644 codemods/create-element-to-jsx/tests/member-child-comments-alt/input.tsx delete mode 100644 codemods/create-element-to-jsx/tests/member-child-comments-alt/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/member-expression-as-prop/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/member-simple-comments/expected.tsx delete mode 100644 codemods/create-element-to-jsx/tests/member-simple-comments/input.tsx delete mode 100644 codemods/create-element-to-jsx/tests/member-simple-comments/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/no-props-arg/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/expected.tsx delete mode 100644 codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/input.tsx delete mode 100644 codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/test.config.json delete mode 100644 codemods/create-element-to-jsx/tests/no-react-valid/expected.tsx delete mode 100644 codemods/create-element-to-jsx/tests/no-react-valid/input.tsx delete mode 100644 codemods/create-element-to-jsx/tests/no-react/input.tsx delete mode 100644 codemods/create-element-to-jsx/tests/object-assign/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/preserve-comments/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/props-array/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/props-boolean/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/props/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/react-spread/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/single-element/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/spread-prop-comments/expected.tsx delete mode 100644 codemods/create-element-to-jsx/tests/spread-prop-comments/input.tsx delete mode 100644 codemods/create-element-to-jsx/tests/spread-prop-comments/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/spread-props/metrics.json delete mode 100644 codemods/create-element-to-jsx/tests/spread/metrics.json delete mode 100644 codemods/error-boundaries/README.md delete mode 100644 codemods/error-boundaries/codemod.yaml delete mode 100644 codemods/error-boundaries/package.json delete mode 100644 codemods/error-boundaries/scripts/codemod.ts delete mode 100644 codemods/error-boundaries/tests/class-component/metrics.json delete mode 100644 codemods/error-boundaries/tests/create-class-component/metrics.json delete mode 100644 codemods/find-dom-node/README.md delete mode 100644 codemods/find-dom-node/codemod.yaml delete mode 100644 codemods/find-dom-node/package.json delete mode 100644 codemods/find-dom-node/scripts/codemod.ts delete mode 100644 codemods/find-dom-node/tests/computed-ref-alias/expected.tsx delete mode 100644 codemods/find-dom-node/tests/computed-ref-alias/input.tsx delete mode 100644 codemods/find-dom-node/tests/computed-ref-alias/metrics.json delete mode 100644 codemods/find-dom-node/tests/deep-refs-chain/expected.tsx delete mode 100644 codemods/find-dom-node/tests/deep-refs-chain/input.tsx delete mode 100644 codemods/find-dom-node/tests/deep-refs-chain/metrics.json delete mode 100644 codemods/find-dom-node/tests/es6-class-no-change/expected.tsx delete mode 100644 codemods/find-dom-node/tests/es6-class-no-change/input.tsx delete mode 100644 codemods/find-dom-node/tests/export-default-no-change/expected.tsx delete mode 100644 codemods/find-dom-node/tests/export-default-no-change/input.tsx delete mode 100644 codemods/find-dom-node/tests/findDOMNode/metrics.json delete mode 100644 codemods/find-dom-node/tests/helper-no-change/expected.tsx delete mode 100644 codemods/find-dom-node/tests/helper-no-change/input.tsx delete mode 100644 codemods/find-dom-node/tests/module-exports/expected.tsx delete mode 100644 codemods/find-dom-node/tests/module-exports/input.tsx delete mode 100644 codemods/find-dom-node/tests/module-exports/metrics.json delete mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx delete mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx delete mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json delete mode 100644 codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json delete mode 100644 codemods/find-dom-node/tests/no-react-create-class/expected.tsx delete mode 100644 codemods/find-dom-node/tests/no-react-create-class/input.tsx create mode 100644 codemods/jssg/react-proptypes-to-prop-types/README.md rename codemods/{ => jssg}/react-proptypes-to-prop-types/codemod.yaml (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/package.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/scripts/codemod.ts (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-import/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-import/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-import/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-named-import/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-named-import/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-named-import/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-require/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-require/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/already-migrated-require/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/assigned-from-react-var/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/assigned-from-react-var/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/assigned-from-react-var/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/default-and-named-import/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/default-import/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/default-import/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/default-import/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/destructured-proptypes-import/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/destructured-proptypes-import/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/import-alias/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/import-alias/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/import-flow-type-with-require/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/mixed-import-and-require/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/mixed-import-and-require/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/module-name/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/module-name/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/module-name/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/module-name/test.config.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/named-parameters/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/no-change-import/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/no-change-import/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/no-change-require/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/no-change-require/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require-alias/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require-alias/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require-destructured-direct/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require-destructured-multi/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require-destructured-only/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require/input.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/require/metrics.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/tests/with-top-comment/input.tsx (100%) rename codemods/{create-element-to-jsx => jssg/react-proptypes-to-prop-types}/tsconfig.json (100%) rename codemods/{ => jssg}/react-proptypes-to-prop-types/workflow.yaml (100%) rename codemods/{ => jssg}/replace-act-import/README.md (53%) rename codemods/{ => jssg}/replace-act-import/codemod.yaml (100%) rename codemods/{ => jssg}/replace-act-import/package.json (100%) rename codemods/{ => jssg}/replace-act-import/scripts/codemod.ts (100%) rename codemods/{ => jssg}/replace-act-import/tests/default-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/default-import/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/default-import/metrics.json (100%) rename codemods/{ => jssg}/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/multiple-named-imports-with-other-specifier/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/multiple-named-imports-with-other-specifier/metrics.json (100%) rename codemods/{ => jssg}/replace-act-import/tests/multiple-named-imports/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/multiple-named-imports/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/multiple-named-imports/metrics.json (100%) rename codemods/{ => jssg}/replace-act-import/tests/named-import-2/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/named-import-2/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/named-import-2/metrics.json (100%) rename codemods/{ => jssg}/replace-act-import/tests/named-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/named-import/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/named-import/metrics.json (100%) rename codemods/{ => jssg}/replace-act-import/tests/named-reexport-no-change/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/named-reexport-no-change/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/other-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/other-import/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/re-export/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/re-export/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/re-export/metrics.json (100%) rename codemods/{ => jssg}/replace-act-import/tests/wildcard-import-2/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/wildcard-import-2/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/wildcard-import-2/metrics.json (100%) rename codemods/{ => jssg}/replace-act-import/tests/wildcard-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/wildcard-import/input.tsx (100%) rename codemods/{ => jssg}/replace-act-import/tests/wildcard-import/metrics.json (100%) rename codemods/{error-boundaries => jssg/replace-act-import}/tsconfig.json (100%) rename codemods/{ => jssg}/replace-act-import/workflow.yaml (100%) rename codemods/{rename-unsafe-lifecycles => jssg/replace-reactdom-render}/.gitignore (100%) create mode 100644 codemods/jssg/replace-reactdom-render/README.md rename codemods/{ => jssg}/replace-reactdom-render/codemod.yaml (100%) rename codemods/{ => jssg}/replace-reactdom-render/package.json (100%) rename codemods/{ => jssg}/replace-reactdom-render/pnpm-lock.yaml (100%) rename codemods/{ => jssg}/replace-reactdom-render/scripts/codemod.ts (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/default/expected.tsx (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/default/input.tsx (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/default/metrics.json (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/multiple-member-render-imports/input.tsx (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/multiple-member-render-imports/metrics.json (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/multiple-named-render-imports/input.tsx (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/multiple-named-render-imports/metrics.json (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/nested/expected.tsx (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/nested/input.tsx (100%) rename codemods/{ => jssg}/replace-reactdom-render/tests/nested/metrics.json (100%) rename codemods/{find-dom-node => jssg/replace-reactdom-render}/tsconfig.json (100%) rename codemods/{create-element-to-jsx => jssg/replace-reactdom-render}/workflow.yaml (100%) rename codemods/{replace-reactdom-render => jssg/replace-string-ref}/.gitignore (100%) rename codemods/{ => jssg}/replace-string-ref/README.md (51%) rename codemods/{ => jssg}/replace-string-ref/codemod.yaml (100%) rename codemods/{ => jssg}/replace-string-ref/package.json (100%) rename codemods/{ => jssg}/replace-string-ref/pnpm-lock.yaml (100%) rename codemods/{ => jssg}/replace-string-ref/scripts/codemod.ts (100%) rename codemods/{ => jssg}/replace-string-ref/tests/class-component-custom-import-names/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/class-component-custom-import-names/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/class-component-custom-import-names/metrics.json (100%) rename codemods/{ => jssg}/replace-string-ref/tests/class-component-default-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/class-component-default-import/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/class-component-default-import/metrics.json (100%) rename codemods/{ => jssg}/replace-string-ref/tests/class-component-named-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/class-component-named-import/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/class-component-named-import/metrics.json (100%) rename codemods/{ => jssg}/replace-string-ref/tests/export-default-class/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/export-default-class/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/export-default-class/metrics.json (100%) rename codemods/{ => jssg}/replace-string-ref/tests/function-component/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/function-component/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/member-expression-ref-value-no-change/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/member-expression-ref-value-no-change/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/mixin-component-no-change/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/mixin-component-no-change/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/multiple-string-refs/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/multiple-string-refs/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/multiple-string-refs/metrics.json (100%) rename codemods/{ => jssg}/replace-string-ref/tests/namespace-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/namespace-import/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/namespace-import/metrics.json (100%) rename codemods/{ => jssg}/replace-string-ref/tests/non-identifier-ref-name/expected.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/non-identifier-ref-name/input.tsx (100%) rename codemods/{ => jssg}/replace-string-ref/tests/non-identifier-ref-name/metrics.json (100%) rename codemods/{pure-component => jssg/replace-string-ref}/tsconfig.json (100%) rename codemods/{error-boundaries => jssg/replace-string-ref}/workflow.yaml (100%) create mode 100644 codemods/jssg/replace-use-form-state/README.md rename codemods/{ => jssg}/replace-use-form-state/codemod.yaml (100%) rename codemods/{ => jssg}/replace-use-form-state/package.json (100%) rename codemods/{ => jssg}/replace-use-form-state/scripts/codemod.ts (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/aliased-to-same-name/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/aliased-to-same-name/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/aliased-to-same-name/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/default-and-named/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/default-and-named/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/default-and-named/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/default-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/default-import/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/default-import/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/member-access-non-import-no-change/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/member-access-non-import-no-change/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/named-import-2/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/named-import-2/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/named-import-2/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/named-import-3/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/named-import-3/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/named-import-3/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/named-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/named-import/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/named-import/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/namespace-and-named/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/namespace-and-named/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/namespace-and-named/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/other-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/other-import/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/shadowed-local-binding/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/shadowed-local-binding/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/shadowed-local-binding/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/type-identifier-rename/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/type-identifier-rename/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/type-identifier-rename/metrics.json (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/wildcard-import/expected.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/wildcard-import/input.tsx (100%) rename codemods/{ => jssg}/replace-use-form-state/tests/wildcard-import/metrics.json (100%) rename codemods/{pure-render-mixin => jssg/replace-use-form-state}/tsconfig.json (100%) rename codemods/{ => jssg}/replace-use-form-state/workflow.yaml (100%) rename codemods/{replace-string-ref => jssg/use-context-hook}/.gitignore (100%) create mode 100644 codemods/jssg/use-context-hook/README.md rename codemods/{ => jssg}/use-context-hook/codemod.yaml (100%) rename codemods/{ => jssg}/use-context-hook/package.json (100%) rename codemods/{update-react-imports => jssg/use-context-hook}/pnpm-lock.yaml (100%) rename codemods/{ => jssg}/use-context-hook/scripts/codemod.ts (100%) rename codemods/{ => jssg}/use-context-hook/tests/aliased-use-context/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/aliased-use-context/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/aliased-use-context/metrics.json (100%) rename codemods/{ => jssg}/use-context-hook/tests/any-use-context/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/any-use-context/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/mixed-import/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/mixed-import/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/mixed-import/metrics.json (100%) rename codemods/{ => jssg}/use-context-hook/tests/multiple-use-context-imports/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/multiple-use-context-imports/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/multiple-use-context-imports/metrics.json (100%) rename codemods/{ => jssg}/use-context-hook/tests/shadowed-local-binding/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/shadowed-local-binding/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/shadowed-local-binding/metrics.json (100%) rename codemods/{ => jssg}/use-context-hook/tests/type-identifier-rename/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/type-identifier-rename/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/type-identifier-rename/metrics.json (100%) rename codemods/{ => jssg}/use-context-hook/tests/typescript-any-use-context/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/typescript-any-use-context/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/typescript-use-context-2/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/typescript-use-context-2/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/typescript-use-context-2/metrics.json (100%) rename codemods/{ => jssg}/use-context-hook/tests/typescript-use-context/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/typescript-use-context/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/typescript-use-context/metrics.json (100%) rename codemods/{ => jssg}/use-context-hook/tests/use-context-2/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/use-context-2/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/use-context-2/metrics.json (100%) rename codemods/{ => jssg}/use-context-hook/tests/use-context/expected.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/use-context/input.tsx (100%) rename codemods/{ => jssg}/use-context-hook/tests/use-context/metrics.json (100%) rename codemods/{react-dom-to-react-dom-factories => jssg/use-context-hook}/tsconfig.json (100%) rename codemods/{find-dom-node => jssg/use-context-hook}/workflow.yaml (100%) create mode 100644 codemods/legacy/.babelrc create mode 100644 codemods/legacy/jest/require-actual.d.ts create mode 100644 codemods/legacy/jest/setup-require-actual.js create mode 100644 codemods/legacy/package.json create mode 100644 codemods/legacy/pnpm-lock.yaml create mode 100644 codemods/legacy/transforms/React-DOM-to-react-dom-factories.js create mode 100644 codemods/legacy/transforms/ReactNative-View-propTypes.js create mode 100644 codemods/legacy/transforms/__testfixtures__/.eslintrc rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-basic-case/input.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.input.js} (100%) rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-basic-case/expected.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.output.js} (100%) rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/input.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.input.js} (100%) rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/expected.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.output.js} (100%) rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/input.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.input.js} (100%) rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/expected.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.output.js} (100%) rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/input.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.input.js} (100%) rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/expected.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.output.js} (100%) rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/expected.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import-dom-from-other-libraries.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import-dom-from-other-libraries.output.js rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-no-change-import/expected.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.output.js rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/expected.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-local-dom-from-other-libraries.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-local-dom-from-other-libraries.output.js rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/expected.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require-dom-from-other-libraries.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require-dom-from-other-libraries.output.js rename codemods/{react-dom-to-react-dom-factories/tests/react-dom-no-change-require/expected.tsx => legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.output.js rename codemods/{react-native-view-prop-types/tests/default-import-multi-reference/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-multi-reference.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/default-import-multi-reference/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-multi-reference.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/default-import-only-reference/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-only-reference.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/default-import-only-reference/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-only-reference.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/default-require-multi-reference/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-multi-reference.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/default-require-multi-reference/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-multi-reference.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/default-require-only-reference/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-only-reference.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/default-require-only-reference/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-only-reference.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/destructured-import-multi-reference/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-multi-reference.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/destructured-import-multi-reference/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-multi-reference.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/destructured-import-only-reference/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-only-reference.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/destructured-import-only-reference/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-only-reference.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/destructured-require-multi-reference/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-multi-reference.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-multi-reference.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/destructured-require-only-reference/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-only-reference.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/destructured-require-only-reference/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-only-reference.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/import-flow-type-with-require/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/import-flow-type-with-require.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/import-flow-type-with-require/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/import-flow-type-with-require.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/multiple-replacements/input.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/multiple-replacements.input.js} (100%) rename codemods/{react-native-view-prop-types/tests/multiple-replacements/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/multiple-replacements.output.js} (100%) rename codemods/{react-native-view-prop-types/tests/noop-import/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-import.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-import.output.js rename codemods/{react-native-view-prop-types/tests/noop-require/expected.tsx => legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-require.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-require.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-anonymous.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-anonymous.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-anonymous2.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-anonymous2.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-create-class-naming.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-create-class-naming.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-displayName.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-displayName.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow1.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow1.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow2.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow2.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow3.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow3.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow4.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow4.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow5.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow5.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow6.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow6.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow7.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-flow7.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-initial-state.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-initial-state.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-no-conversion.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-no-conversion.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-no-display-name.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-no-display-name.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-property-field.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-property-field.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-prune-react.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-prune-react.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-prune-react2.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-prune-react2.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-prune-react3.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-prune-react3.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-prune-react4.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-prune-react4.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin1.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin1.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin2.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin2.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin3.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin3.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin4.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin4.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin5.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin5.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-test2.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-test2.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-top-comment.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class-top-comment.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/class.output.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/export-default-class.input.js create mode 100644 codemods/legacy/transforms/__testfixtures__/class/export-default-class.output.js rename codemods/{create-element-to-jsx/tests/allow-member-expression/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-allow-member-expression.input.js} (100%) rename codemods/{create-element-to-jsx/tests/allow-member-expression/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-allow-member-expression.output.js} (100%) rename codemods/{create-element-to-jsx/tests/arg-spread/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-arg-spread.input.js} (100%) rename codemods/{create-element-to-jsx/tests/arg-spread/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-arg-spread.output.js} (100%) rename codemods/{create-element-to-jsx/tests/call-as-children/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-call-as-children.input.js} (100%) rename codemods/{create-element-to-jsx/tests/call-as-children/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-call-as-children.output.js} (100%) rename codemods/{create-element-to-jsx/tests/call-expression-as-prop/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-call-expression-as-prop.input.js} (100%) rename codemods/{create-element-to-jsx/tests/call-expression-as-prop/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-call-expression-as-prop.output.js} (100%) rename codemods/{create-element-to-jsx/tests/children-literal/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-children-literal.input.js} (100%) rename codemods/{create-element-to-jsx/tests/children-literal/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-children-literal.output.js} (100%) rename codemods/{create-element-to-jsx/tests/children-map/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-children-map.input.js} (100%) rename codemods/{create-element-to-jsx/tests/children-map/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-children-map.output.js} (100%) rename codemods/{create-element-to-jsx/tests/children-mixed-empty-string/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-children-mixed-empty-string.input.js} (100%) rename codemods/{create-element-to-jsx/tests/children-mixed-empty-string/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-children-mixed-empty-string.output.js} (100%) rename codemods/{create-element-to-jsx/tests/children/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-children.input.js} (100%) rename codemods/{create-element-to-jsx/tests/children/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-children.output.js} (100%) rename codemods/{create-element-to-jsx/tests/computed-component/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-computed-component.input.js} (100%) rename codemods/{create-element-to-jsx/tests/computed-component/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-computed-component.output.js} (100%) rename codemods/{create-element-to-jsx/tests/deep-nesting/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-deep-nesting.input.js} (100%) rename codemods/{create-element-to-jsx/tests/deep-nesting/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-deep-nesting.output.js} (100%) rename codemods/{create-element-to-jsx/tests/element-comment-positioning/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-element-comment-positioning.input.js} (100%) rename codemods/{create-element-to-jsx/tests/element-comment-positioning/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-element-comment-positioning.output.js} (100%) rename codemods/{create-element-to-jsx/tests/escaped-string/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-escaped-string.input.js} (100%) rename codemods/{create-element-to-jsx/tests/escaped-string/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-escaped-string.output.js} (100%) rename codemods/{create-element-to-jsx/tests/gt-lt-entities/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-gt-lt-entities.input.js} (100%) rename codemods/{create-element-to-jsx/tests/gt-lt-entities/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-gt-lt-entities.output.js} (100%) rename codemods/{create-element-to-jsx/tests/ignore-bad-capitalization/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-ignore-bad-capitalization.input.js} (100%) rename codemods/{create-element-to-jsx/tests/ignore-bad-capitalization/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-ignore-bad-capitalization.output.js} (100%) rename codemods/{create-element-to-jsx/tests/literal-prop/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-literal-prop.input.js} (100%) rename codemods/{create-element-to-jsx/tests/literal-prop/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-literal-prop.output.js} (100%) rename codemods/{create-element-to-jsx/tests/literal-spacing/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-literal-spacing.input.js} (100%) rename codemods/{create-element-to-jsx/tests/literal-spacing/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-literal-spacing.output.js} (100%) rename codemods/{create-element-to-jsx/tests/member-expression-as-prop/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-member-expression-as-prop.input.js} (100%) rename codemods/{create-element-to-jsx/tests/member-expression-as-prop/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-member-expression-as-prop.output.js} (100%) rename codemods/{create-element-to-jsx/tests/no-props-arg/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-no-props-arg.input.js} (100%) rename codemods/{create-element-to-jsx/tests/no-props-arg/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-no-props-arg.output.js} (100%) rename codemods/{create-element-to-jsx/tests/no-react/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-no-react.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-no-react.output.js rename codemods/{create-element-to-jsx/tests/object-assign/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-object-assign.input.js} (100%) rename codemods/{create-element-to-jsx/tests/object-assign/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-object-assign.output.js} (100%) rename codemods/{create-element-to-jsx/tests/preserve-comments/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-preserve-comments.input.js} (100%) rename codemods/{create-element-to-jsx/tests/preserve-comments/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-preserve-comments.output.js} (100%) rename codemods/{create-element-to-jsx/tests/props-array/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-props-array.input.js} (100%) rename codemods/{create-element-to-jsx/tests/props-array/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-props-array.output.js} (100%) rename codemods/{create-element-to-jsx/tests/props-boolean/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-props-boolean.input.js} (100%) rename codemods/{create-element-to-jsx/tests/props-boolean/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-props-boolean.output.js} (100%) rename codemods/{create-element-to-jsx/tests/props/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-props.input.js} (100%) rename codemods/{create-element-to-jsx/tests/props/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-props.output.js} (100%) rename codemods/{create-element-to-jsx/tests/react-spread/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-react-spread.input.js} (100%) rename codemods/{create-element-to-jsx/tests/react-spread/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-react-spread.output.js} (100%) rename codemods/{create-element-to-jsx/tests/single-element/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-single-element.input.js} (100%) rename codemods/{create-element-to-jsx/tests/single-element/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-single-element.output.js} (100%) rename codemods/{create-element-to-jsx/tests/spread-props/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-spread-props.input.js} (100%) rename codemods/{create-element-to-jsx/tests/spread-props/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-spread-props.output.js} (100%) rename codemods/{create-element-to-jsx/tests/spread/input.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-spread.input.js} (100%) rename codemods/{create-element-to-jsx/tests/spread/expected.tsx => legacy/transforms/__testfixtures__/create-element-to-jsx-spread.output.js} (100%) rename codemods/{sort-comp/tests => legacy/transforms/__testfixtures__}/custom-sort-group/.eslintrc (100%) rename codemods/{sort-comp/tests/custom-sort-group/input.tsx => legacy/transforms/__testfixtures__/custom-sort-group/custom-sort-group.input.js} (100%) rename codemods/{sort-comp/tests/custom-sort-group/expected.tsx => legacy/transforms/__testfixtures__/custom-sort-group/custom-sort-group.output.js} (100%) rename codemods/{sort-comp/tests => legacy/transforms/__testfixtures__}/custom-sort/.eslintrc (100%) rename codemods/{sort-comp/tests/custom-sort/input.tsx => legacy/transforms/__testfixtures__/custom-sort/custom-sort.input.js} (100%) rename codemods/{sort-comp/tests/custom-sort/expected.tsx => legacy/transforms/__testfixtures__/custom-sort/custom-sort.output.js} (100%) rename codemods/{error-boundaries/tests/class-component/input.tsx => legacy/transforms/__testfixtures__/error-boundaries/class-component.input.js} (100%) rename codemods/{error-boundaries/tests/class-component/expected.tsx => legacy/transforms/__testfixtures__/error-boundaries/class-component.output.js} (100%) rename codemods/{error-boundaries/tests/create-class-component/input.tsx => legacy/transforms/__testfixtures__/error-boundaries/create-class-component.input.js} (100%) rename codemods/{error-boundaries/tests/create-class-component/expected.tsx => legacy/transforms/__testfixtures__/error-boundaries/create-class-component.output.js} (100%) rename codemods/{find-dom-node/tests/findDOMNode/input.tsx => legacy/transforms/__testfixtures__/findDOMNode.input.js} (100%) rename codemods/{find-dom-node/tests/findDOMNode/expected.tsx => legacy/transforms/__testfixtures__/findDOMNode.output.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow1/input.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow1.input.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow1/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow1.output.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow10/input.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow10.input.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow10/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow10.output.js} (98%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow11/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow11.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow11.output.js rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow2/input.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow2.input.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow2/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow2.output.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow3/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow3.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow3.output.js rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow4/input.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow4.input.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow4/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow4.output.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow5/input.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow5.input.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow5/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow5.output.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow6/input.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow6.input.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow6/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow6.output.js} (97%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow7/input.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow7.input.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow7/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow7.output.js} (95%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow8/input.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow8.input.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow8/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow8.output.js} (60%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow9/input.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow9.input.js} (100%) rename codemods/{manual-bind-to-arrow/tests/manual-bind-to-arrow9/expected.tsx => legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow9.output.js} (98%) rename codemods/{pure-component/tests/destructuring/input.tsx => legacy/transforms/__testfixtures__/pure-component-destructuring.input.js} (100%) rename codemods/{pure-component/tests/destructuring/expected.tsx => legacy/transforms/__testfixtures__/pure-component-destructuring.output.js} (100%) rename codemods/{pure-component/tests/default/input.tsx => legacy/transforms/__testfixtures__/pure-component.input.js} (100%) rename codemods/{pure-component/tests/default/expected.tsx => legacy/transforms/__testfixtures__/pure-component.output.js} (100%) rename codemods/{pure-component/tests/arrow/input.tsx => legacy/transforms/__testfixtures__/pure-component2.input.js} (100%) rename codemods/{pure-component/tests/arrow/expected.tsx => legacy/transforms/__testfixtures__/pure-component2.output.js} (100%) rename codemods/{pure-render-mixin/tests/pure-render-mixin/input.tsx => legacy/transforms/__testfixtures__/pure-render-mixin.input.js} (100%) rename codemods/{pure-render-mixin/tests/pure-render-mixin/expected.tsx => legacy/transforms/__testfixtures__/pure-render-mixin.output.js} (100%) rename codemods/{pure-render-mixin/tests/pure-render-mixin2/input.tsx => legacy/transforms/__testfixtures__/pure-render-mixin2.input.js} (100%) rename codemods/{pure-render-mixin/tests/pure-render-mixin2/expected.tsx => legacy/transforms/__testfixtures__/pure-render-mixin2.output.js} (100%) rename codemods/{pure-render-mixin/tests/pure-render-mixin3/input.tsx => legacy/transforms/__testfixtures__/pure-render-mixin3.input.js} (100%) rename codemods/{pure-render-mixin/tests/pure-render-mixin3/expected.tsx => legacy/transforms/__testfixtures__/pure-render-mixin3.output.js} (100%) rename codemods/{pure-render-mixin/tests/pure-render-mixin4/input.tsx => legacy/transforms/__testfixtures__/pure-render-mixin4.input.js} (100%) rename codemods/{pure-render-mixin/tests/pure-render-mixin4/expected.tsx => legacy/transforms/__testfixtures__/pure-render-mixin4.output.js} (100%) rename codemods/{react-to-react-dom/tests/import-dom-base/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-dom-base.input.js} (100%) rename codemods/{react-to-react-dom/tests/import-dom-base/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-dom-base.output.js} (100%) rename codemods/{react-to-react-dom/tests/import-multiple-specifiers/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-multiple-specifiers.input.js} (100%) rename codemods/{react-to-react-dom/tests/import-multiple-specifiers/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-multiple-specifiers.output.js} (100%) rename codemods/{react-to-react-dom/tests/import-server-base/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-server-base.input.js} (100%) rename codemods/{react-to-react-dom/tests/import-server-base/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-server-base.output.js} (100%) rename codemods/{react-to-react-dom/tests/import-with-existing-react-dom/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-with-existing-react-dom.input.js} (100%) rename codemods/{react-to-react-dom/tests/import-with-existing-react-dom/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-with-existing-react-dom.output.js} (100%) rename codemods/{react-to-react-dom/tests/import-without-default-specifier/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-without-default-specifier.input.js} (100%) rename codemods/{react-to-react-dom/tests/import-without-default-specifier/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/import-without-default-specifier.output.js} (100%) rename codemods/{react-to-react-dom/tests/mixed-with-existing-react-dom/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/mixed-with-existing-react-dom.input.js} (100%) rename codemods/{react-to-react-dom/tests/mixed-with-existing-react-dom/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/mixed-with-existing-react-dom.output.js} (100%) rename codemods/{react-to-react-dom/tests/require-dom-base/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/require-dom-base.input.js} (100%) rename codemods/{react-to-react-dom/tests/require-dom-base/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/require-dom-base.output.js} (100%) rename codemods/{react-to-react-dom/tests/require-indirect/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/require-indirect.input.js} (100%) rename codemods/{react-to-react-dom/tests/require-indirect/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/require-indirect.output.js} (100%) rename codemods/{react-to-react-dom/tests/require-keeps-react/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/require-keeps-react.input.js} (100%) rename codemods/{react-to-react-dom/tests/require-keeps-react/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/require-keeps-react.output.js} (100%) rename codemods/{react-to-react-dom/tests/require-server-base/input.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/require-server-base.input.js} (100%) rename codemods/{react-to-react-dom/tests/require-server-base/expected.tsx => legacy/transforms/__testfixtures__/react-to-react-dom/require-server-base.output.js} (100%) rename codemods/{remove-context-provider/tests/no-provider/expected.tsx => legacy/transforms/__testfixtures__/remove-context-provider/no-provider.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/remove-context-provider/no-provider.output.js rename codemods/{remove-context-provider/tests/typescript-no-provider/expected.tsx => legacy/transforms/__testfixtures__/remove-context-provider/typescript/no-provider.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/no-provider.output.js rename codemods/{remove-context-provider/tests/typescript-with-provider-2/input.tsx => legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider-2.input.js} (100%) rename codemods/{remove-context-provider/tests/typescript-no-provider/input.tsx => legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider-2.output.js} (100%) rename codemods/{remove-context-provider/tests/typescript-with-provider/input.tsx => legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider.input.js} (100%) rename codemods/{remove-context-provider/tests/typescript-with-provider/expected.tsx => legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider.output.js} (100%) rename codemods/{remove-context-provider/tests/with-provider-2/input.tsx => legacy/transforms/__testfixtures__/remove-context-provider/with-provider-2.input.js} (100%) rename codemods/{remove-context-provider/tests/no-provider/input.tsx => legacy/transforms/__testfixtures__/remove-context-provider/with-provider-2.output.js} (100%) rename codemods/{remove-context-provider/tests/with-provider/input.tsx => legacy/transforms/__testfixtures__/remove-context-provider/with-provider.input.js} (100%) rename codemods/{remove-context-provider/tests/with-provider/expected.tsx => legacy/transforms/__testfixtures__/remove-context-provider/with-provider.output.js} (100%) rename codemods/{remove-forward-ref/tests/arrow-function-expression/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/arrow-function-expression.input.js} (100%) rename codemods/{remove-forward-ref/tests/arrow-function-expression/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/arrow-function-expression.output.js} (100%) rename codemods/{remove-forward-ref/tests/callee-is-member-expression/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/callee-is-member-expression.input.js} (100%) rename codemods/{remove-forward-ref/tests/callee-is-member-expression/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/callee-is-member-expression.output.js} (100%) rename codemods/{remove-forward-ref/tests/forward-ref-import-2/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import-2.input.js} (100%) rename codemods/{remove-forward-ref/tests/forward-ref-import-2/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import-2.output.js} (100%) rename codemods/{remove-forward-ref/tests/forward-ref-import/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import.input.js} (100%) rename codemods/{remove-forward-ref/tests/forward-ref-import/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import.output.js} (100%) rename codemods/{remove-forward-ref/tests/function-expression/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/function-expression.input.js} (100%) rename codemods/{remove-forward-ref/tests/function-expression/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/function-expression.output.js} (100%) rename codemods/{remove-forward-ref/tests/props-identifier/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/props-identifier.input.js} (100%) rename codemods/{remove-forward-ref/tests/props-identifier/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/props-identifier.output.js} (100%) rename codemods/{remove-forward-ref/tests/props-object-pattern/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/props-object-pattern.input.js} (100%) rename codemods/{remove-forward-ref/tests/props-object-pattern/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/props-object-pattern.output.js} (100%) rename codemods/{remove-forward-ref/tests/typescript-props-type-literal/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.input.js} (100%) rename codemods/{remove-forward-ref/tests/typescript-props-type-literal/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.output.js} (100%) rename codemods/{remove-forward-ref/tests/typescript-type-arguments-custom-names/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.input.js} (100%) rename codemods/{remove-forward-ref/tests/typescript-type-arguments-custom-names/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.output.js} (100%) rename codemods/{remove-forward-ref/tests/typescript-type-arguments-type-literals/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.input.js} (100%) rename codemods/{remove-forward-ref/tests/typescript-type-arguments-type-literals/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.output.js} (100%) rename codemods/{remove-forward-ref/tests/typescript-type-arguments/input.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.input.js} (100%) rename codemods/{remove-forward-ref/tests/typescript-type-arguments/expected.tsx => legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.output.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/arrow-functions/input.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/arrow-functions.input.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/arrow-functions.output.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/create-react-class/input.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/create-react-class.input.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/create-react-class/expected.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/create-react-class.output.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/instance-methods/input.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/instance-methods.input.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/instance-methods/expected.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/instance-methods.output.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/manually-calling-lifecycles/input.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-calling-lifecycles.input.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/manually-calling-lifecycles/expected.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-calling-lifecycles.output.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/input.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-invoked-mixin-methods.input.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/expected.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-invoked-mixin-methods.output.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/input.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/one-lifecycle-calls-another.input.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/expected.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/one-lifecycle-calls-another.output.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/standalone-function/expected.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/standalone-function.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/standalone-function.output.js rename codemods/{rename-unsafe-lifecycles/tests/typescript-class-tsx/input.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/typescript/class.tsx.input.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/typescript-class-tsx/expected.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/typescript/class.tsx.output.js} (100%) rename codemods/{rename-unsafe-lifecycles/tests/variable-within-class-method/expected.tsx => legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/variable-within-class-method.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/variable-within-class-method.output.js rename codemods/{sort-comp/tests/sort-comp-pure/input.tsx => legacy/transforms/__testfixtures__/sort-comp-pure.input.js} (100%) rename codemods/{sort-comp/tests/sort-comp-pure/expected.tsx => legacy/transforms/__testfixtures__/sort-comp-pure.output.js} (100%) rename codemods/{sort-comp/tests/sort-comp/input.tsx => legacy/transforms/__testfixtures__/sort-comp.input.js} (100%) rename codemods/{sort-comp/tests/sort-comp/expected.tsx => legacy/transforms/__testfixtures__/sort-comp.output.js} (100%) rename codemods/{sort-comp/tests/sort-comp2/input.tsx => legacy/transforms/__testfixtures__/sort-comp2.input.js} (100%) rename codemods/{sort-comp/tests/sort-comp2/expected.tsx => legacy/transforms/__testfixtures__/sort-comp2.output.js} (100%) rename codemods/{sort-comp/tests/sort-comp3/input.tsx => legacy/transforms/__testfixtures__/sort-comp3.input.js} (100%) rename codemods/{sort-comp/tests/sort-comp3/expected.tsx => legacy/transforms/__testfixtures__/sort-comp3.output.js} (100%) rename codemods/{update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import-react-variable.input.js} (100%) rename codemods/{update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import-react-variable.output.js} (99%) rename codemods/{update-react-imports/tests/default-and-multiple-specifiers-import/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import.input.js} (100%) rename codemods/{update-react-imports/tests/default-and-multiple-specifiers-import/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import.output.js} (99%) rename codemods/{update-react-imports/tests/destructure-named-imports-react-not-removed/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-react-not-removed.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-react-not-removed.output.js rename codemods/{update-react-imports/tests/destructure-named-imports-variable-used/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-variable-used.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-variable-used.output.js rename codemods/{update-react-imports/tests/destructure-named-imports/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports.output.js rename codemods/{update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import-react-variable.input.js} (100%) rename codemods/{update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import-react-variable.output.js} (99%) rename codemods/{update-react-imports/tests/flow-default-and-type-specifier-import/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import.input.js} (100%) rename codemods/{update-react-imports/tests/flow-default-and-type-specifier-import/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import.output.js} (98%) rename codemods/{update-react-imports/tests/jsx-element/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/jsx-element.input.js} (100%) rename codemods/{update-react-imports/tests/jsx-element/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/jsx-element.output.js} (100%) rename codemods/{update-react-imports/tests/jsx-fragment/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/jsx-fragment.input.js} (100%) rename codemods/{update-react-imports/tests/jsx-fragment/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/jsx-fragment.output.js} (100%) rename codemods/{update-react-imports/tests/leading-comment/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/leading-comment.input.js} (100%) rename codemods/{update-react-imports/tests/leading-comment/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/leading-comment.output.js} (100%) rename codemods/{update-react-imports/tests/react-already-used-named-export/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-already-used-named-export.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/update-react-imports/react-already-used-named-export.output.js rename codemods/{update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element-react-variable.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element-react-variable.output.js} (98%) rename codemods/{update-react-imports/tests/react-basic-default-export-jsx-element/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element.input.js} (100%) rename codemods/{update-react-imports/tests/react-basic-default-export-jsx-element/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element.output.js} (100%) rename codemods/{update-react-imports/tests/react-basic-default-export/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-react-basic-default-export-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export.output.js} (98%) rename codemods/{update-react-imports/tests/react-jsx-member-expression/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-jsx-member-expression.input.js} (100%) rename codemods/{update-react-imports/tests/react-jsx-member-expression/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-jsx-member-expression.output.js} (75%) rename codemods/{update-react-imports/tests/react-not-removed/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-not-removed.input.js} (100%) rename codemods/{update-react-imports/tests/destructure-named-imports-react-not-removed/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-not-removed.output.js} (99%) rename codemods/{update-react-imports/tests/react-type-default-export/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-type-default-export.input.js} (100%) rename codemods/{update-react-imports/tests/react-type-default-export/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-type-default-export.output.js} (97%) rename codemods/{update-react-imports/tests/react-type-not-removed/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-type-not-removed.input.js} (100%) rename codemods/{update-react-imports/tests/react-type-not-removed/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/react-type-not-removed.output.js} (98%) rename codemods/{update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import-react-variable.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import-react-variable.tsx.output.js} (99%) rename codemods/{update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import.tsx.output.js} (98%) rename codemods/{update-react-imports/tests/typescript-jsx-element-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-element.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-jsx-element-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-element.tsx.output.js} (100%) rename codemods/{update-react-imports/tests/typescript-jsx-fragment-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-fragment.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-jsx-fragment-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-fragment.tsx.output.js} (100%) rename codemods/{update-react-imports/tests/typescript-leading-comment-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/leading-comment.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-leading-comment-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/leading-comment.tsx.output.js} (100%) rename codemods/{update-react-imports/tests/typescript-preserve-types-default-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-default.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-preserve-types-namespace-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-default.tsx.output.js} (100%) rename codemods/{update-react-imports/tests/typescript-preserve-types-default-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-namespace.tsx.input.js} (99%) create mode 100644 codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-namespace.tsx.output.js rename codemods/{update-react-imports/tests/typescript-react-already-used-named-export-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-already-used-named-export.tsx.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-already-used-named-export.tsx.output.js rename codemods/{update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element-react-variable.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element-react-variable.tsx.output.js} (98%) rename codemods/{update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element.tsx.output.js} (100%) rename codemods/{update-react-imports/tests/typescript-react-basic-default-export-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/react-basic-default-export/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export.tsx.output.js} (98%) rename codemods/{update-react-imports/tests/typescript-react-jsx-member-expression-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-jsx-member-expression.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-react-jsx-member-expression-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-jsx-member-expression.tsx.output.js} (75%) rename codemods/{update-react-imports/tests/typescript-react-not-removed-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-not-removed.tsx.input.js} (100%) rename codemods/{update-react-imports/tests/typescript-react-not-removed-tsx/expected.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/react-not-removed.tsx.output.js} (85%) rename codemods/{update-react-imports/tests/typescript-variable-already-used-tsx/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/typescript/variable-already-used.tsx.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/variable-already-used.tsx.output.js rename codemods/{update-react-imports/tests/variable-already-used/input.tsx => legacy/transforms/__testfixtures__/update-react-imports/variable-already-used.input.js} (100%) create mode 100644 codemods/legacy/transforms/__testfixtures__/update-react-imports/variable-already-used.output.js create mode 100644 codemods/legacy/transforms/__tests__/React-DOM-to-react-dom-factories-test.js create mode 100644 codemods/legacy/transforms/__tests__/ReactNative-View-propTypes-test.js create mode 100644 codemods/legacy/transforms/__tests__/class-test.js create mode 100644 codemods/legacy/transforms/__tests__/create-element-to-jsx-test.js create mode 100644 codemods/legacy/transforms/__tests__/custom-sort-group.js create mode 100644 codemods/legacy/transforms/__tests__/custom-sort.js create mode 100644 codemods/legacy/transforms/__tests__/error-boundaries.js create mode 100644 codemods/legacy/transforms/__tests__/findDOMNode-test.js create mode 100644 codemods/legacy/transforms/__tests__/manual-bind-to-arrow-test.js create mode 100644 codemods/legacy/transforms/__tests__/pure-component-test.js create mode 100644 codemods/legacy/transforms/__tests__/pure-render-mixin-test.js create mode 100644 codemods/legacy/transforms/__tests__/react-to-react-dom-test.js create mode 100644 codemods/legacy/transforms/__tests__/remove-context-provider.test.ts create mode 100644 codemods/legacy/transforms/__tests__/remove-forward-ref.test.ts create mode 100644 codemods/legacy/transforms/__tests__/rename-unsafe-lifecycles-test.js create mode 100644 codemods/legacy/transforms/__tests__/sort-comp-test.js create mode 100644 codemods/legacy/transforms/__tests__/update-react-imports-test.js create mode 100644 codemods/legacy/transforms/class.js create mode 100644 codemods/legacy/transforms/create-element-to-jsx.js create mode 100644 codemods/legacy/transforms/error-boundaries.js create mode 100644 codemods/legacy/transforms/findDOMNode.js create mode 100644 codemods/legacy/transforms/manual-bind-to-arrow.js create mode 100644 codemods/legacy/transforms/pure-component.js create mode 100644 codemods/legacy/transforms/pure-render-mixin.js create mode 100644 codemods/legacy/transforms/react-to-react-dom.js create mode 100644 codemods/legacy/transforms/remove-context-provider.ts create mode 100644 codemods/legacy/transforms/remove-forward-ref.ts create mode 100644 codemods/legacy/transforms/rename-unsafe-lifecycles.js create mode 100644 codemods/legacy/transforms/sort-comp.js create mode 100644 codemods/legacy/transforms/update-react-imports.js create mode 100644 codemods/legacy/transforms/utils/ReactUtils.js create mode 100644 codemods/legacy/transforms/utils/array-polyfills.js create mode 100644 codemods/legacy/transforms/utils/doesNotUseArguments.js create mode 100644 codemods/legacy/tsconfig.json delete mode 100644 codemods/manual-bind-to-arrow/README.md delete mode 100644 codemods/manual-bind-to-arrow/codemod.yaml delete mode 100644 codemods/manual-bind-to-arrow/package.json delete mode 100644 codemods/manual-bind-to-arrow/scripts/codemod.ts delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/metrics.json delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/metrics.json delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/input.tsx delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/metrics.json delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/input.tsx delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/metrics.json delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/metrics.json delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/metrics.json delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/metrics.json delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/metrics.json delete mode 100644 codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/metrics.json delete mode 100644 codemods/manual-bind-to-arrow/tsconfig.json delete mode 100644 codemods/manual-bind-to-arrow/workflow.yaml delete mode 100644 codemods/pure-component/README.md delete mode 100644 codemods/pure-component/codemod.yaml delete mode 100644 codemods/pure-component/package.json delete mode 100644 codemods/pure-component/scripts/codemod.ts delete mode 100644 codemods/pure-component/scripts/differential-tests.mjs delete mode 100644 codemods/pure-component/scripts/warning-tests.mjs delete mode 100644 codemods/pure-component/tests/arrow/metrics.json delete mode 100644 codemods/pure-component/tests/arrow/test.config.json delete mode 100644 codemods/pure-component/tests/default/metrics.json delete mode 100644 codemods/pure-component/tests/destructuring/metrics.json delete mode 100644 codemods/pure-component/tests/destructuring/test.config.json delete mode 100644 codemods/pure-component/tests/export-default-arrow/expected.tsx delete mode 100644 codemods/pure-component/tests/export-default-arrow/input.tsx delete mode 100644 codemods/pure-component/tests/export-default-arrow/metrics.json delete mode 100644 codemods/pure-component/tests/export-default-arrow/test.config.json delete mode 100644 codemods/pure-component/tests/export-default-function/expected.tsx delete mode 100644 codemods/pure-component/tests/export-default-function/input.tsx delete mode 100644 codemods/pure-component/tests/export-default-function/metrics.json delete mode 100644 codemods/pure-component/tests/mixed-superclasses/expected.tsx delete mode 100644 codemods/pure-component/tests/mixed-superclasses/input.tsx delete mode 100644 codemods/pure-component/tests/mixed-superclasses/metrics.json delete mode 100644 codemods/pure-component/tests/named-export-arrow/expected.tsx delete mode 100644 codemods/pure-component/tests/named-export-arrow/input.tsx delete mode 100644 codemods/pure-component/tests/named-export-arrow/metrics.json delete mode 100644 codemods/pure-component/tests/named-export-arrow/test.config.json delete mode 100644 codemods/pure-component/tests/named-export/expected.tsx delete mode 100644 codemods/pure-component/tests/named-export/input.tsx delete mode 100644 codemods/pure-component/tests/named-export/metrics.json delete mode 100644 codemods/pure-component/tests/named-import-component/expected.tsx delete mode 100644 codemods/pure-component/tests/named-import-component/input.tsx delete mode 100644 codemods/pure-component/tests/named-import-component/metrics.json delete mode 100644 codemods/pure-component/tests/named-import-purecomponent-alias/expected.tsx delete mode 100644 codemods/pure-component/tests/named-import-purecomponent-alias/input.tsx delete mode 100644 codemods/pure-component/tests/named-import-purecomponent-alias/metrics.json delete mode 100644 codemods/pure-component/tests/react-native-named-import/expected.tsx delete mode 100644 codemods/pure-component/tests/react-native-named-import/input.tsx delete mode 100644 codemods/pure-component/tests/react-native-named-import/metrics.json delete mode 100644 codemods/pure-component/workflow.yaml delete mode 100644 codemods/pure-render-mixin/README.md delete mode 100644 codemods/pure-render-mixin/codemod.yaml delete mode 100644 codemods/pure-render-mixin/package.json delete mode 100644 codemods/pure-render-mixin/scripts/codemod.ts delete mode 100644 codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx delete mode 100644 codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx delete mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx delete mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx delete mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json delete mode 100644 codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json delete mode 100644 codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx delete mode 100644 codemods/pure-render-mixin/tests/no-react-no-change/input.tsx delete mode 100644 codemods/pure-render-mixin/tests/pure-render-mixin/metrics.json delete mode 100644 codemods/pure-render-mixin/tests/pure-render-mixin2/metrics.json delete mode 100644 codemods/pure-render-mixin/tests/pure-render-mixin3/metrics.json delete mode 100644 codemods/pure-render-mixin/tests/pure-render-mixin4/metrics.json delete mode 100644 codemods/pure-render-mixin/tests/pure-render-mixin4/test.config.json delete mode 100644 codemods/pure-render-mixin/workflow.yaml delete mode 100644 codemods/react-dom-to-react-dom-factories/README.md delete mode 100644 codemods/react-dom-to-react-dom-factories/codemod.yaml delete mode 100644 codemods/react-dom-to-react-dom-factories/package.json delete mode 100644 codemods/react-dom-to-react-dom-factories/scripts/codemod.ts delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/metrics.json delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/metrics.json delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/metrics.json delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/metrics.json delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/input.tsx delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/input.tsx delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/input.tsx delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/expected.tsx delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/input.tsx delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/input.tsx delete mode 100644 codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/input.tsx delete mode 100644 codemods/react-dom-to-react-dom-factories/workflow.yaml delete mode 100644 codemods/react-native-view-prop-types/README.md delete mode 100644 codemods/react-native-view-prop-types/codemod.yaml delete mode 100644 codemods/react-native-view-prop-types/package.json delete mode 100644 codemods/react-native-view-prop-types/scripts/codemod.ts delete mode 100644 codemods/react-native-view-prop-types/tests/default-import-multi-reference/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/default-import-only-reference/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/default-require-multi-reference/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/default-require-only-reference/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/destructured-import-only-reference/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/destructured-require-only-reference/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/import-flow-type-with-require/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/multiple-replacements/metrics.json delete mode 100644 codemods/react-native-view-prop-types/tests/noop-import/input.tsx delete mode 100644 codemods/react-native-view-prop-types/tests/noop-require/input.tsx delete mode 100644 codemods/react-native-view-prop-types/tsconfig.json delete mode 100644 codemods/react-native-view-prop-types/workflow.yaml delete mode 100644 codemods/react-proptypes-to-prop-types/README.md delete mode 100644 codemods/react-proptypes-to-prop-types/tsconfig.json delete mode 100644 codemods/react-to-react-dom/README.md delete mode 100644 codemods/react-to-react-dom/codemod.yaml delete mode 100644 codemods/react-to-react-dom/package.json delete mode 100644 codemods/react-to-react-dom/scripts/codemod.ts delete mode 100644 codemods/react-to-react-dom/scripts/error-tests.mjs delete mode 100644 codemods/react-to-react-dom/tests/import-dom-base-uppercase/expected.tsx delete mode 100644 codemods/react-to-react-dom/tests/import-dom-base-uppercase/input.tsx delete mode 100644 codemods/react-to-react-dom/tests/import-dom-base-uppercase/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/import-dom-base/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/import-dom-type-identifier/expected.tsx delete mode 100644 codemods/react-to-react-dom/tests/import-dom-type-identifier/input.tsx delete mode 100644 codemods/react-to-react-dom/tests/import-dom-type-identifier/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/import-multiple-specifiers/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/import-server-base/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/import-with-existing-react-dom/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/import-without-default-specifier/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/require-dom-base-uppercase/expected.tsx delete mode 100644 codemods/react-to-react-dom/tests/require-dom-base-uppercase/input.tsx delete mode 100644 codemods/react-to-react-dom/tests/require-dom-base-uppercase/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/require-dom-base/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/require-indirect-mixed/expected.tsx delete mode 100644 codemods/react-to-react-dom/tests/require-indirect-mixed/input.tsx delete mode 100644 codemods/react-to-react-dom/tests/require-indirect-mixed/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/require-indirect/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/require-keeps-react/metrics.json delete mode 100644 codemods/react-to-react-dom/tests/require-server-base/metrics.json delete mode 100644 codemods/react-to-react-dom/tsconfig.json delete mode 100644 codemods/react-to-react-dom/workflow.yaml delete mode 100644 codemods/remove-context-provider/README.md delete mode 100644 codemods/remove-context-provider/codemod.yaml delete mode 100644 codemods/remove-context-provider/package.json delete mode 100644 codemods/remove-context-provider/scripts/codemod.ts delete mode 100644 codemods/remove-context-provider/tests/nested-member-no-change/expected.tsx delete mode 100644 codemods/remove-context-provider/tests/nested-member-no-change/input.tsx delete mode 100644 codemods/remove-context-provider/tests/typescript-with-provider-2/expected.tsx delete mode 100644 codemods/remove-context-provider/tests/typescript-with-provider-2/metrics.json delete mode 100644 codemods/remove-context-provider/tests/typescript-with-provider/metrics.json delete mode 100644 codemods/remove-context-provider/tests/with-provider-2/expected.tsx delete mode 100644 codemods/remove-context-provider/tests/with-provider-2/metrics.json delete mode 100644 codemods/remove-context-provider/tests/with-provider/metrics.json delete mode 100644 codemods/remove-context-provider/tsconfig.json delete mode 100644 codemods/remove-context-provider/workflow.yaml delete mode 100644 codemods/remove-forward-ref/README.md delete mode 100644 codemods/remove-forward-ref/codemod.yaml delete mode 100644 codemods/remove-forward-ref/package.json delete mode 100644 codemods/remove-forward-ref/scripts/codemod.ts delete mode 100644 codemods/remove-forward-ref/tests/arrow-function-expression/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/callee-is-member-expression/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/forward-ref-import-2/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/forward-ref-import/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/function-expression/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/expected.tsx delete mode 100644 codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/input.tsx delete mode 100644 codemods/remove-forward-ref/tests/invalid-render-arg-no-change/expected.tsx delete mode 100644 codemods/remove-forward-ref/tests/invalid-render-arg-no-change/input.tsx delete mode 100644 codemods/remove-forward-ref/tests/multiple-react-imports/expected.tsx delete mode 100644 codemods/remove-forward-ref/tests/multiple-react-imports/input.tsx delete mode 100644 codemods/remove-forward-ref/tests/multiple-react-imports/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/props-identifier/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/props-object-pattern/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/typescript-props-type-literal/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/typescript-qualified-names/expected.tsx delete mode 100644 codemods/remove-forward-ref/tests/typescript-qualified-names/input.tsx delete mode 100644 codemods/remove-forward-ref/tests/typescript-qualified-names/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/typescript-type-arguments/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-props-union/expected.tsx delete mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-props-union/input.tsx delete mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-props-union/metrics.json delete mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/expected.tsx delete mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/input.tsx delete mode 100644 codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/metrics.json delete mode 100644 codemods/remove-forward-ref/tsconfig.json delete mode 100644 codemods/remove-forward-ref/workflow.yaml delete mode 100644 codemods/rename-unsafe-lifecycles/README.md delete mode 100644 codemods/rename-unsafe-lifecycles/codemod.yaml delete mode 100644 codemods/rename-unsafe-lifecycles/package.json delete mode 100644 codemods/rename-unsafe-lifecycles/pnpm-lock.yaml delete mode 100644 codemods/rename-unsafe-lifecycles/scripts/codemod.ts delete mode 100644 codemods/rename-unsafe-lifecycles/tests/arrow-functions/metrics.json delete mode 100644 codemods/rename-unsafe-lifecycles/tests/create-react-class/metrics.json delete mode 100644 codemods/rename-unsafe-lifecycles/tests/instance-methods/metrics.json delete mode 100644 codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/metrics.json delete mode 100644 codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/metrics.json delete mode 100644 codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/metrics.json delete mode 100644 codemods/rename-unsafe-lifecycles/tests/standalone-function/input.tsx delete mode 100644 codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/metrics.json delete mode 100644 codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/input.tsx delete mode 100644 codemods/rename-unsafe-lifecycles/tsconfig.json delete mode 100644 codemods/rename-unsafe-lifecycles/workflow.yaml delete mode 100644 codemods/replace-act-import/tsconfig.json delete mode 100644 codemods/replace-reactdom-render/README.md delete mode 100644 codemods/replace-reactdom-render/tsconfig.json delete mode 100644 codemods/replace-reactdom-render/workflow.yaml delete mode 100644 codemods/replace-string-ref/tsconfig.json delete mode 100644 codemods/replace-string-ref/workflow.yaml delete mode 100644 codemods/replace-use-form-state/README.md delete mode 100644 codemods/replace-use-form-state/tsconfig.json delete mode 100644 codemods/sort-comp/README.md delete mode 100644 codemods/sort-comp/codemod.yaml delete mode 100644 codemods/sort-comp/package.json delete mode 100644 codemods/sort-comp/scripts/codemod.ts delete mode 100644 codemods/sort-comp/tests/custom-sort-group/metrics.json delete mode 100644 codemods/sort-comp/tests/custom-sort/metrics.json delete mode 100644 codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx delete mode 100644 codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx delete mode 100644 codemods/sort-comp/tests/methods-order-option/expected.tsx delete mode 100644 codemods/sort-comp/tests/methods-order-option/input.tsx delete mode 100644 codemods/sort-comp/tests/methods-order-option/metrics.json delete mode 100644 codemods/sort-comp/tests/methods-order-option/test.config.json delete mode 100644 codemods/sort-comp/tests/no-react-explicit-require/expected.tsx delete mode 100644 codemods/sort-comp/tests/no-react-explicit-require/input.tsx delete mode 100644 codemods/sort-comp/tests/no-react-explicit-require/metrics.json delete mode 100644 codemods/sort-comp/tests/no-react-explicit-require/test.config.json delete mode 100644 codemods/sort-comp/tests/no-react-no-change/expected.tsx delete mode 100644 codemods/sort-comp/tests/no-react-no-change/input.tsx delete mode 100644 codemods/sort-comp/tests/sort-comp-pure/metrics.json delete mode 100644 codemods/sort-comp/tests/sort-comp/metrics.json delete mode 100644 codemods/sort-comp/tests/sort-comp2/metrics.json delete mode 100644 codemods/sort-comp/tests/sort-comp3/metrics.json delete mode 100644 codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx delete mode 100644 codemods/sort-comp/tests/unrelated-component-no-change/input.tsx delete mode 100644 codemods/sort-comp/tsconfig.json delete mode 100644 codemods/sort-comp/workflow.yaml delete mode 100644 codemods/update-react-imports/.gitignore delete mode 100644 codemods/update-react-imports/README.md delete mode 100644 codemods/update-react-imports/codemod.yaml delete mode 100644 codemods/update-react-imports/package.json delete mode 100644 codemods/update-react-imports/scripts/codemod.ts delete mode 100644 codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/metrics.json delete mode 100644 codemods/update-react-imports/tests/default-and-multiple-specifiers-import/metrics.json delete mode 100644 codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/metrics.json delete mode 100644 codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/test.config.json delete mode 100644 codemods/update-react-imports/tests/destructure-named-imports-variable-used/input.tsx delete mode 100644 codemods/update-react-imports/tests/destructure-named-imports-variable-used/metrics.json delete mode 100644 codemods/update-react-imports/tests/destructure-named-imports-variable-used/test.config.json delete mode 100644 codemods/update-react-imports/tests/destructure-named-imports/expected.tsx delete mode 100644 codemods/update-react-imports/tests/destructure-named-imports/metrics.json delete mode 100644 codemods/update-react-imports/tests/destructure-named-imports/test.config.json delete mode 100644 codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/metrics.json delete mode 100644 codemods/update-react-imports/tests/flow-default-and-type-specifier-import/metrics.json delete mode 100644 codemods/update-react-imports/tests/jsx-element/metrics.json delete mode 100644 codemods/update-react-imports/tests/jsx-fragment/metrics.json delete mode 100644 codemods/update-react-imports/tests/leading-comment/metrics.json delete mode 100644 codemods/update-react-imports/tests/react-already-used-named-export/expected.tsx delete mode 100644 codemods/update-react-imports/tests/react-already-used-named-export/metrics.json delete mode 100644 codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/metrics.json delete mode 100644 codemods/update-react-imports/tests/react-basic-default-export-jsx-element/metrics.json delete mode 100644 codemods/update-react-imports/tests/react-basic-default-export/metrics.json delete mode 100644 codemods/update-react-imports/tests/react-jsx-member-expression/metrics.json delete mode 100644 codemods/update-react-imports/tests/react-not-removed/expected.tsx delete mode 100644 codemods/update-react-imports/tests/react-not-removed/metrics.json delete mode 100644 codemods/update-react-imports/tests/react-type-default-export/metrics.json delete mode 100644 codemods/update-react-imports/tests/react-type-not-removed/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-jsx-element-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-leading-comment-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/expected.tsx delete mode 100644 codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/expected.tsx delete mode 100644 codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-react-not-removed-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/typescript-variable-already-used-tsx/expected.tsx delete mode 100644 codemods/update-react-imports/tests/typescript-variable-already-used-tsx/metrics.json delete mode 100644 codemods/update-react-imports/tests/variable-already-used/expected.tsx delete mode 100644 codemods/update-react-imports/tests/variable-already-used/metrics.json delete mode 100644 codemods/update-react-imports/tsconfig.json delete mode 100644 codemods/update-react-imports/workflow.yaml delete mode 100644 codemods/use-context-hook/.gitignore delete mode 100644 codemods/use-context-hook/README.md delete mode 100644 codemods/use-context-hook/pnpm-lock.yaml delete mode 100644 codemods/use-context-hook/tsconfig.json delete mode 100644 codemods/use-context-hook/workflow.yaml create mode 100644 scripts/validate-docs.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d6b15d2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + pull_request: + +jobs: + docs-and-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.14.2 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run lint + + active: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.14.2 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm run test:active + - run: pnpm run check-types:active + + legacy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 9.14.2 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + - run: pnpm run test:legacy diff --git a/LEGACY.md b/LEGACY.md new file mode 100644 index 0000000..b815337 --- /dev/null +++ b/LEGACY.md @@ -0,0 +1,40 @@ +# Legacy Codemods + +This branch keeps the non-high-priority `react-codemod` transforms as a legacy jscodeshift snapshot under [`codemods/legacy/transforms/`](./codemods/legacy/transforms/). + +The legacy snapshot is preserved for source reference, upstream-style tests, and branch preview parity with the structure of `reactjs/react-codemod`. + +## Legacy Catalog + +- [`create-element-to-jsx`](./codemods/legacy/transforms/create-element-to-jsx.js) +- [`error-boundaries`](./codemods/legacy/transforms/error-boundaries.js) +- [`findDOMNode`](./codemods/legacy/transforms/findDOMNode.js) +- [`manual-bind-to-arrow`](./codemods/legacy/transforms/manual-bind-to-arrow.js) +- [`pure-component`](./codemods/legacy/transforms/pure-component.js) +- [`pure-render-mixin`](./codemods/legacy/transforms/pure-render-mixin.js) +- [`React-DOM-to-react-dom-factories`](./codemods/legacy/transforms/React-DOM-to-react-dom-factories.js) +- [`ReactNative-View-propTypes`](./codemods/legacy/transforms/ReactNative-View-propTypes.js) +- [`react-to-react-dom`](./codemods/legacy/transforms/react-to-react-dom.js) +- [`remove-context-provider`](./codemods/legacy/transforms/remove-context-provider.ts) +- [`remove-forward-ref`](./codemods/legacy/transforms/remove-forward-ref.ts) +- [`rename-unsafe-lifecycles`](./codemods/legacy/transforms/rename-unsafe-lifecycles.js) +- [`sort-comp`](./codemods/legacy/transforms/sort-comp.js) +- [`update-react-imports`](./codemods/legacy/transforms/update-react-imports.js) +- [`class`](./codemods/legacy/transforms/class.js) + +## Notes + +- The 6 promoted JSSG transforms are not duplicated in `codemods/legacy/`. +- This preview branch does not claim that legacy transforms can be run through Codemod Registry. +- The legacy snapshot keeps the full upstream-style test corpus in `codemods/legacy/transforms/__tests__`. +- The preview branch CI runs the compatible legacy subset under the current preview runtime: + +```bash +pnpm run test:legacy +``` + +- To run the full legacy snapshot test corpus manually from `codemods/legacy/`: + +```bash +pnpm test +``` diff --git a/PARITY_STATUS.md b/PARITY_STATUS.md new file mode 100644 index 0000000..4271f0c --- /dev/null +++ b/PARITY_STATUS.md @@ -0,0 +1,41 @@ +# Legacy Parity Status + +Last updated: 2026-04-15 + +Status meanings: + +- `Certified`: replacement-grade rollout confidence. Strict/default package tests are green, public test posture is portable, and there are no known rollout-blocking logic gaps versus the legacy jscodeshift codemod. `Certified` may include intentional safe extensions on legacy-unsupported edge cases, as long as intentional legacy behavior remains unbroken. +- `Legacy Snapshot`: preserved as upstream jscodeshift source and tests on this branch, but not promoted as part of the active JSSG replacement surface. + +## Active JSSG Surface + +These are the only codemods promoted as active replacements on this branch. + +| Codemod | Legacy Source | Status | Notes | +| --- | --- | --- | --- | +| `replace-use-form-state` | `replace-use-form-state.ts` | `Certified` | Legacy fixture surface plus added multi-import regression coverage are green. Collection-wide replacement on matching `react-dom` imports fixes a real parity gap, and the broader multi-import handling is treated as an acceptable safe extension where legacy only partially migrated files. | +| `replace-act-import` | `replace-act-import.ts` | `Certified` | Legacy fixture surface plus added multi-import regression coverage. This closes a real parity gap, and the mixed test-utils partial-removal path is an acceptable safe extension that does not break intentional legacy behavior. | +| `replace-string-ref` | `replace-string-ref.ts` | `Certified` | Legacy fixture surface plus added namespace/default-export/multi-ref coverage are green, and the direct-superclass guard restores the intended direct-superclass behavior by avoiding the prior mixin false positive. | +| `replace-reactdom-render` | `replace-reactdom-render.ts` | `Certified` | Legacy fixtures plus added multi-alias regression coverage are green. The extra multi-alias handling is an acceptable safe extension over legacy implementation limits; it preserves intentional legacy behavior while covering additional valid cases. | +| `react-proptypes-to-prop-types` | `React-PropTypes-to-prop-types.js` | `Certified` | Full legacy fixture surface is green, targeted mixed-import validation is aligned, and no JSSG-specific rollout blocker was found in the audit. No additional extension beyond the legacy behavior is currently relied on for certification. | +| `use-context-hook` | `use-context-hook.ts` | `Certified` | Legacy fixture surface plus added multi-import regression coverage are green. The multi-import handling goes beyond the legacy implementation, but it is treated as an acceptable safe extension because it preserves intentional legacy behavior while covering additional valid cases. | + +## Legacy Snapshot Surface + +These codemods remain available only as an upstream jscodeshift snapshot under [`codemods/legacy/transforms/`](./codemods/legacy/transforms/): + +- `class` +- `create-element-to-jsx` +- `error-boundaries` +- `findDOMNode` +- `manual-bind-to-arrow` +- `pure-component` +- `pure-render-mixin` +- `React-DOM-to-react-dom-factories` +- `ReactNative-View-propTypes` +- `react-to-react-dom` +- `remove-context-provider` +- `remove-forward-ref` +- `rename-unsafe-lifecycles` +- `sort-comp` +- `update-react-imports` diff --git a/README.md b/README.md index d8c9916..a1af4ef 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,42 @@ -# jssg-react-codemods - -React codemods built with JSSG and JavaScript ast-grep. - -## Packages - -- `@react/error-boundaries` -- `@react/find-dom-node` -- `@react/manual-bind-to-arrow` -- `@react/pure-render-mixin` -- `@react/react-proptypes-to-prop-types` -- `@react/rename-unsafe-lifecycles` -- `@react/replace-act-import` -- `@react/replace-reactdom-render` -- `@react/replace-string-ref` -- `@react/replace-use-form-state` -- `@react/sort-comp` -- `@react/update-react-imports` -- `@react/use-context-hook` +# React Codemods Preview + +This branch is a React-facing preview of how this repo should look as a replacement-style home for `react-codemod`. + +It promotes 6 audited JSSG codemods as the active surface and preserves the remaining upstream jscodeshift codemods as a legacy snapshot. + +## Active JSSG Transforms + +These transforms are the active replacement surface on this branch. They are source-first JSSG codemods and are not described here as Codemod Registry entries unless they are actually published. + +- [`use-context-hook`](./codemods/jssg/use-context-hook/) — replace `useContext` usage with `use` +- [`replace-act-import`](./codemods/jssg/replace-act-import/) — move `act` from `react-dom/test-utils` to `react` +- [`replace-string-ref`](./codemods/jssg/replace-string-ref/) — replace deprecated string refs with callback refs +- [`replace-use-form-state`](./codemods/jssg/replace-use-form-state/) — rename `useFormState` to `useActionState` +- [`replace-reactdom-render`](./codemods/jssg/replace-reactdom-render/) — replace `ReactDOM.render` with `createRoot(...).render(...)` +- [`react-proptypes-to-prop-types`](./codemods/jssg/react-proptypes-to-prop-types/) — replace `React.PropTypes` with `prop-types` + +## Legacy Codemods + +All non-priority transforms are preserved as an upstream-style legacy jscodeshift snapshot under [`codemods/legacy/`](./codemods/legacy/). + +See [LEGACY.md](./LEGACY.md) for the legacy catalog and testing notes. ## Development ```bash -pnpm test -pnpm -r check-types +pnpm install +pnpm run ci +``` + +Run active JSSG checks only: + +```bash +pnpm run test:active +pnpm run check-types:active ``` -Run a single package: +Run the legacy snapshot test harness: ```bash -pnpm --filter @react-codemods/replace-reactdom-render test +pnpm run test:legacy ``` diff --git a/codemods/create-element-to-jsx/README.md b/codemods/create-element-to-jsx/README.md deleted file mode 100644 index 31f2cae..0000000 --- a/codemods/create-element-to-jsx/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/create-element-to-jsx - -Convert `React.createElement(...)` calls to JSX. - -## Usage - -```bash -codemod run @react/create-element-to-jsx -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/create-element-to-jsx/codemod.yaml b/codemods/create-element-to-jsx/codemod.yaml deleted file mode 100644 index 3b0f069..0000000 --- a/codemods/create-element-to-jsx/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/create-element-to-jsx" -version: "0.1.0" -description: "Convert React.createElement calls to JSX" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React", "JSX"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/create-element-to-jsx/package.json b/codemods/create-element-to-jsx/package.json deleted file mode 100644 index 743d67d..0000000 --- a/codemods/create-element-to-jsx/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@react/create-element-to-jsx", - "version": "0.1.0", - "description": "Convert React.createElement calls to JSX", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/error-tests.mjs ./scripts/differential-tests.mjs", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "@types/node": "latest", - "typescript": "latest" - } -} diff --git a/codemods/create-element-to-jsx/scripts/codemod.ts b/codemods/create-element-to-jsx/scripts/codemod.ts deleted file mode 100644 index 458ea1c..0000000 --- a/codemods/create-element-to-jsx/scripts/codemod.ts +++ /dev/null @@ -1,1277 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -type Conversion = { - code: string; - count: number; - hasComments: boolean; -}; - -type TagDescriptor = { - text: string; - identifierComments: string[]; -}; - -type AttrDescriptor = { - leadingBlocks: string[]; - inline: string; - hasComments: boolean; -}; - -type RenderedElement = { - core: string; - count: number; - leadingComments: string[]; - trailingComments: string[]; - hasComments: boolean; - expressionTrailingComments?: string[]; -}; - -type ArgumentLayout = { - args: SgNode[]; - boundaryComments: SgNode[][]; -}; - -const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -function lineIndent(source: string, index: number): string { - let lineStart = index; - while (lineStart > 0 && source[lineStart - 1] !== "\n") { - lineStart--; - } - - let end = lineStart; - while (end < source.length && (source[end] === " " || source[end] === "\t")) { - end++; - } - - return source.slice(lineStart, end); -} - -function indentAll(text: string, indent: string): string { - return text - .split("\n") - .map((line) => `${indent}${line}`) - .join("\n"); -} - -function prefixFirstLine(text: string, prefix: string): string { - const newline = text.indexOf("\n"); - if (newline === -1) { - return `${prefix}${text}`; - } - - return `${prefix}${text.slice(0, newline)}${text.slice(newline)}`; -} - -function startsLowercase(value: string): boolean { - const first = value[0]; - return first !== undefined && first >= "a" && first <= "z"; -} - -function encodeJSXTextValue(value: string): string { - return value.replaceAll("<", "<").replaceAll(">", ">"); -} - -function stringValue(node: SgNode): string { - if (node.kind() !== "string") { - throw new Error(`Expected string node, got "${node.kind()}"`); - } - - const evaluated = Function(`"use strict"; return (${node.text()});`)() as unknown; - if (typeof evaluated !== "string") { - throw new Error(`Expected string literal, got "${typeof evaluated}"`); - } - - return evaluated; -} - -function isReactModuleName(value: string | null): boolean { - return value !== null && REACT_MODULES.has(value); -} - -function stringNodeFrom(node: SgNode): SgNode | null { - if (node.kind() === "string") { - return node; - } - - return node.find({ rule: { kind: "string" } }); -} - -function requireSource(call: SgNode): string | null { - const callee = call.field("function"); - if (!callee || callee.kind() !== "identifier" || callee.text() !== "require") { - return null; - } - - const args = callArguments(call); - const firstArg = args[0]; - if (!firstArg || firstArg.kind() !== "string") { - return null; - } - - return stringValue(firstArg); -} - -function importSource(node: SgNode): string | null { - const sourceNode = node.field("source") ?? stringNodeFrom(node); - if (!sourceNode || sourceNode.kind() !== "string") { - return null; - } - - return stringValue(sourceNode); -} - -function hasReact(rootNode: SgNode): boolean { - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (isReactModuleName(importSource(importNode))) { - return true; - } - } - - for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - if (isReactModuleName(requireSource(call))) { - return true; - } - } - - return false; -} - -function callArguments(call: SgNode): SgNode[] { - const args = call.field("arguments"); - if (!args) { - return []; - } - - return args.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function isReactCreateElementCall(node: SgNode): boolean { - if (node.kind() !== "call_expression") { - return false; - } - - const callee = node.field("function"); - if (!callee || callee.kind() !== "member_expression") { - return false; - } - - const object = callee.field("object"); - const property = callee.field("property"); - - return object?.kind() === "identifier" && - object.text() === "React" && - property?.kind() === "property_identifier" && - property.text() === "createElement"; -} - -function closestCreateElementAncestor(node: SgNode): SgNode | null { - for (const ancestor of node.ancestors()) { - if (isReactCreateElementCall(ancestor)) { - return ancestor; - } - } - - return null; -} - -function ownCommentTexts(call: SgNode): string[] { - return call.findAll({ rule: { kind: "comment" } }) - .filter((comment) => closestCreateElementAncestor(comment)?.id() === call.id()) - .sort((left, right) => left.range().start.index - right.range().start.index) - .map((comment) => comment.text()); -} - -function ownCommentNodes(call: SgNode): SgNode[] { - return call.findAll({ rule: { kind: "comment" } }) - .filter((comment) => closestCreateElementAncestor(comment)?.id() === call.id()) - .sort((left, right) => left.range().start.index - right.range().start.index); -} - -function directCommentNodes(node: SgNode | null): SgNode[] { - if (!node) { - return []; - } - - return node.children() - .filter((child) => child.kind() === "comment") - .sort((left, right) => left.range().start.index - right.range().start.index); -} - -function directCommentTexts(node: SgNode | null): string[] { - return directCommentNodes(node).map((comment) => comment.text()); -} - -function subtractComments(comments: SgNode[], excluded: SgNode[]): SgNode[] { - const excludedIds = new Set(excluded.map((comment) => comment.id())); - return comments.filter((comment) => !excludedIds.has(comment.id())); -} - -function splitDirectComments(node: SgNode, comments = directCommentNodes(node)): { - before: SgNode[]; - after: SgNode[]; - all: SgNode[]; -} { - const nodeText = node.text(); - const nodeStart = node.range().start.index; - const nodeEnd = node.range().end.index; - let tokenStart = nodeStart; - let tokenEnd = nodeEnd; - - if (comments.length > 0) { - const start = Math.min(nodeStart, ...comments.map((comment) => comment.range().start.index)); - const end = Math.max(nodeEnd, ...comments.map((comment) => comment.range().end.index)); - const segment = node.getRoot().root().text().slice(start, end); - const tokenOffset = segment.indexOf(nodeText); - if (tokenOffset >= 0) { - tokenStart = start + tokenOffset; - tokenEnd = tokenStart + nodeText.length; - } - } - - return { - before: comments.filter((comment) => comment.range().end.index <= tokenStart), - after: comments.filter((comment) => comment.range().start.index >= tokenEnd), - all: comments, - }; -} - -function joinCommentTexts(comments: SgNode[], source: string): string { - if (comments.length === 0) { - return ""; - } - - let text = comments[0]!.text(); - for (let i = 1; i < comments.length; i++) { - const previous = comments[i - 1]!; - const current = comments[i]!; - const between = source.slice(previous.range().end.index, current.range().start.index); - text += between.includes("\n") ? "" : between; - text += current.text(); - } - - return text; -} - -function commentsPrefixText(comments: SgNode[], nextStart: number, source: string): string { - if (comments.length === 0) { - return ""; - } - - const last = comments[comments.length - 1]!; - return `${joinCommentTexts(comments, source)}${source.slice(last.range().end.index, nextStart)}`; -} - -function trailingInlineCommentTexts(comments: SgNode[], anchorEnd: number, source: string): string[] { - if (comments.length === 0) { - return []; - } - - const texts: string[] = []; - let cursor = anchorEnd; - for (const comment of comments) { - texts.push(`${source.slice(cursor, comment.range().start.index)}${comment.text()}`); - cursor = comment.range().end.index; - } - return texts; -} - -function tokenSourceWithComments(node: SgNode, rendered: string, source: string): string { - const split = splitDirectComments(node); - const beforeStart = split.before.length > 0 - ? Math.min(...split.before.map((comment) => comment.range().start.index)) - : node.range().start.index; - const beforeText = source.slice(beforeStart, node.range().start.index); - const afterEnd = split.after.length > 0 - ? Math.max(...split.after.map((comment) => comment.range().end.index)) - : node.range().end.index; - const afterText = source.slice(node.range().end.index, afterEnd); - return `${beforeText}${rendered}${afterText}`; -} - -function commentBlocks(comments: SgNode[], source: string): string[] { - if (comments.length === 0) { - return []; - } - - const blocks = [comments[0]!.text()]; - for (let i = 1; i < comments.length; i++) { - const previous = comments[i - 1]!; - const current = comments[i]!; - const between = source.slice(previous.range().end.index, current.range().start.index); - if (between.includes("\n")) { - blocks.push(current.text()); - } else { - blocks[blocks.length - 1] += between + current.text(); - } - } - - return blocks; -} - -function isLineComment(text: string): boolean { - return text.startsWith("//"); -} - -function argumentLayout(call: SgNode): ArgumentLayout { - const argsNode = call.field("arguments"); - if (!argsNode) { - return { args: [], boundaryComments: [[]] }; - } - - const rawChildren = argsNode.children(); - const args: SgNode[] = []; - const boundaryComments: SgNode[][] = [[]]; - let seenArgs = 0; - - for (const child of rawChildren) { - if (child.kind() === "comment") { - boundaryComments[seenArgs]!.push(child); - continue; - } - - if (child.isNamed()) { - args.push(child); - seenArgs++; - if (!boundaryComments[seenArgs]) { - boundaryComments[seenArgs] = []; - } - } - } - - return { args, boundaryComments }; -} - -function detachedLeadingComments(call: SgNode): SgNode[] { - const parent = call.parent(); - if (!parent || parent.kind() === "arguments" || parent.kind() === "call_expression") { - return []; - } - - return parent.children() - .filter((child) => child.kind() === "comment" && child.range().end.index <= call.range().start.index) - .sort((left, right) => left.range().start.index - right.range().start.index); -} - -function splitBoundaryCommentsByComma( - anchorNode: SgNode, - comments: SgNode[], - source: string, -): { beforeComma: SgNode[]; afterComma: SgNode[] } { - const beforeComma: SgNode[] = []; - const afterComma: SgNode[] = []; - for (const comment of comments) { - const between = source.slice(anchorNode.range().end.index, comment.range().start.index); - if (between.includes(",")) { - afterComma.push(comment); - } else { - beforeComma.push(comment); - } - } - return { beforeComma, afterComma }; -} - -function tagDescriptor(node: SgNode): TagDescriptor | null { - if (node.kind() === "identifier" || node.kind() === "property_identifier") { - return { text: node.text(), identifierComments: directCommentTexts(node) }; - } - - if (node.kind() === "string") { - return { text: stringValue(node), identifierComments: directCommentTexts(node) }; - } - - if (node.kind() !== "member_expression") { - return null; - } - - const object = node.field("object"); - const property = node.field("property"); - if (!object || !property) { - return null; - } - - const objectTag = tagDescriptor(object); - const propertyTag = tagDescriptor(property); - if (!objectTag || !propertyTag) { - return null; - } - - return { - text: `${objectTag.text}.${propertyTag.text}`, - identifierComments: [ - ...directCommentTexts(node), - ...objectTag.identifierComments, - ...propertyTag.identifierComments, - ], - }; -} - -function renderTokenWithComments(node: SgNode, text: string, source: string): string { - return tokenSourceWithComments(node, text, source); -} - -function renderPropValue(node: SgNode, source: string): string { - if (node.kind() === "string") { - const literal = stringValue(node); - if (canLiteralBePropString(node, literal)) { - return renderTokenWithComments(node, `"${literal}"`, source); - } - } - - const split = splitDirectComments(node); - return `{${joinCommentTexts(split.before, source)}${node.text()}${joinCommentTexts(split.after, source)}}`; -} - -function spreadAttrDescriptor(node: SgNode, source: string): AttrDescriptor { - const argument = spreadArgument(node); - if (!argument) { - throw new Error("Expected spread element argument"); - } - - return { - leadingBlocks: commentBlocks(directCommentNodes(node), source), - inline: `{...${tokenSourceWithComments(argument, argument.text(), source)}}`, - hasComments: directCommentNodes(node).length > 0 || directCommentNodes(argument).length > 0, - }; -} - -function pairInlineDescriptor( - node: SgNode, - source: string, - prefixInlineComments: string[], - suffixInlineComments: string[], - leadingBlocks: string[], -): AttrDescriptor { - const key = node.field("key"); - const value = node.field("value"); - if (!key || !value) { - throw new Error("Expected object property to have key and value"); - } - - const pairComments = directCommentNodes(node); - const keyAfter = pairComments[0] ? [pairComments[0]!] : []; - const valueBefore = pairComments.slice(1); - const keyText = `${prefixInlineComments.join("")}${attrName(key)}${joinCommentTexts(keyAfter, source)}`; - const valueText = (() => { - if (value.kind() === "string") { - const literal = stringValue(value); - if (canLiteralBePropString(value, literal)) { - return `${commentsPrefixText(valueBefore, value.range().start.index, source)}"${literal}"${suffixInlineComments.join("")}`; - } - } - return `{${commentsPrefixText(valueBefore, value.range().start.index, source)}${value.text()}}${suffixInlineComments.join("")}`; - })(); - - return { - leadingBlocks, - inline: `${keyText}=${valueText}`, - hasComments: - leadingBlocks.length > 0 || - prefixInlineComments.length > 0 || - suffixInlineComments.length > 0 || - pairComments.length > 0, - }; -} - -function propsToDescriptors(node: SgNode | null, source: string): { - attrs: AttrDescriptor[]; - trailingElementComments: string[]; - consumedBoundaryComments: string[]; -} { - if (!node || node.kind() === "null") { - return { attrs: [], trailingElementComments: [], consumedBoundaryComments: [] }; - } - - if (isMemberCall(node, "React", "__spread") || isMemberCall(node, "Object", "assign")) { - const callee = node.field("function"); - const calleeObject = callee?.field("object") ?? null; - const calleeProperty = callee?.field("property") ?? null; - const attrs: AttrDescriptor[] = []; - const trailing = [ - ...directCommentTexts(node), - ...directCommentTexts(callee), - ...directCommentTexts(calleeObject), - ...directCommentTexts(calleeProperty), - ]; - - for (const arg of callArguments(node)) { - const nested = propsToDescriptors(arg, source); - attrs.push(...nested.attrs); - trailing.push(...nested.trailingElementComments); - } - - return { attrs, trailingElementComments: trailing, consumedBoundaryComments: [] }; - } - - if ( - node.kind() === "identifier" || - node.kind() === "member_expression" || - node.kind() === "call_expression" - ) { - return { - attrs: [{ - leadingBlocks: [], - inline: `{...${tokenSourceWithComments(node, node.text(), source)}}`, - hasComments: directCommentNodes(node).length > 0, - }], - trailingElementComments: [], - consumedBoundaryComments: [], - }; - } - - if (node.kind() !== "object") { - throw new Error(`Unexpected attribute of type "${legacyType(node)}"`); - } - - const attrs: AttrDescriptor[] = []; - const objectComments = directCommentNodes(node); - const namedChildrenList = node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); - const pendingLeadingBlocks = new Map(); - const consumedObjectComments = new Set(); - - if (namedChildrenList.length === 0) { - return { - attrs, - trailingElementComments: objectComments.map((comment) => comment.text()), - consumedBoundaryComments: [], - }; - } - - for (let index = 0; index < namedChildrenList.length; index++) { - const child = namedChildrenList[index]!; - const nextChild = namedChildrenList[index + 1]; - - if (child.kind() === "spread_element") { - attrs.push(spreadAttrDescriptor(child, source)); - continue; - } - - if (child.kind() !== "pair") { - continue; - } - - const key = child.field("key"); - const value = child.field("value"); - if (!key || !value) { - continue; - } - - const commentsBefore = objectComments.filter((comment) => - !consumedObjectComments.has(comment.id()) && - comment.range().end.index <= key.range().start.index && - comment.range().start.index >= node.range().start.index - ); - const commentsAfterValue = objectComments.filter((comment) => - !consumedObjectComments.has(comment.id()) && - comment.range().start.index >= value.range().end.index && - comment.range().end.index <= (nextChild?.range().start.index ?? node.range().end.index) - ); - - const beforeComma: SgNode[] = []; - const afterComma: SgNode[] = []; - for (const comment of commentsAfterValue) { - const between = source.slice(value.range().end.index, comment.range().start.index); - if (between.includes(",")) { - afterComma.push(comment); - } else { - beforeComma.push(comment); - } - } - - const trailingBlocksForCurrent = !nextChild - ? (commentsBefore.length > 0 && afterComma.length > 0 - ? [`${commentBlocks(commentsBefore, source).join("")}${afterComma.map((comment) => comment.text()).join("")}`] - : commentBlocks(afterComma, source)) - : []; - const currentLeadingBlocks = [ - ...(pendingLeadingBlocks.get(index) ?? []), - ...((!nextChild && afterComma.length > 0) - ? [] - : (index === 0 ? [] : commentBlocks(commentsBefore, source))), - ...trailingBlocksForCurrent, - ]; - const inlinePrefix = index === 0 ? commentsBefore.map((comment) => comment.text()) : []; - const inlineSuffix = beforeComma.map((comment) => comment.text()); - - attrs.push(pairInlineDescriptor(child, source, inlinePrefix, inlineSuffix, currentLeadingBlocks)); - [...commentsBefore, ...beforeComma, ...afterComma].forEach((comment) => consumedObjectComments.add(comment.id())); - - const carryBlocks = nextChild ? commentBlocks(afterComma, source) : []; - if (carryBlocks.length > 0) { - const targetIndex = nextChild ? index + 1 : index; - pendingLeadingBlocks.set(targetIndex, [ - ...(pendingLeadingBlocks.get(targetIndex) ?? []), - ...carryBlocks, - ]); - } - } - - return { - attrs, - trailingElementComments: [], - consumedBoundaryComments: [], - }; -} - -function jsxReference(node: SgNode): string | null { - if (node.kind() === "identifier" || node.kind() === "property_identifier") { - return node.text(); - } - - if (node.kind() === "string") { - return stringValue(node); - } - - if (node.kind() !== "member_expression") { - return null; - } - - const object = node.field("object"); - const property = node.field("property"); - if (!object || !property) { - return null; - } - - const objectRef = jsxReference(object); - const propertyRef = jsxReference(property); - if (!objectRef || !propertyRef) { - return null; - } - - return `${objectRef}.${propertyRef}`; -} - -function topLevelJsxTag(node: SgNode): string | null { - const reference = jsxReference(node); - if (!reference) { - return null; - } - - if (node.kind() === "identifier" && startsLowercase(node.text())) { - return null; - } - - if (node.kind() === "string" && !startsLowercase(stringValue(node))) { - return null; - } - - return reference; -} - -function legacyType(node: SgNode): string { - switch (node.kind()) { - case "call_expression": - return "CallExpression"; - case "identifier": - return "Identifier"; - case "member_expression": - return "MemberExpression"; - case "object": - return "ObjectExpression"; - case "spread_element": - return "SpreadElement"; - case "string": - case "number": - case "true": - case "false": - case "null": - return "Literal"; - default: - return node.kind(); - } -} - -function canLiteralBePropString(node: SgNode, value: string): boolean { - return !node.text().includes("\\") && !value.includes("\""); -} - -function spreadArgument(node: SgNode): SgNode | null { - return node.children().find((child) => child.isNamed() && child.kind() !== "comment") ?? null; -} - -function attrName(node: SgNode): string { - if (node.kind() === "property_identifier" || node.kind() === "identifier") { - return node.text(); - } - - if (node.kind() === "string") { - return stringValue(node); - } - - throw new Error(`Unexpected property key type "${legacyType(node)}"`); -} - -function pairToAttribute(pair: SgNode): string { - const key = pair.field("key"); - const value = pair.field("value"); - if (!key || !value) { - throw new Error("Expected object property to have key and value"); - } - - const name = attrName(key); - if (value.kind() === "string") { - const literal = stringValue(value); - if (canLiteralBePropString(value, literal)) { - return `${name}="${literal}"`; - } - } - - return `${name}={${value.text()}}`; -} - -function isMemberCall(node: SgNode, objectName: string, propertyName: string): boolean { - if (node.kind() !== "call_expression") { - return false; - } - - const callee = node.field("function"); - if (!callee || callee.kind() !== "member_expression") { - return false; - } - - const object = callee.field("object"); - const property = callee.field("property"); - - return object?.kind() === "identifier" && - object.text() === objectName && - property?.kind() === "property_identifier" && - property.text() === propertyName; -} - -function propsToAttributes(node: SgNode | null): string[] { - if (!node || node.kind() === "null") { - return []; - } - - if (isMemberCall(node, "React", "__spread") || isMemberCall(node, "Object", "assign")) { - return callArguments(node).flatMap((arg) => propsToAttributes(arg)); - } - - if ( - node.kind() === "identifier" || - node.kind() === "member_expression" || - node.kind() === "call_expression" - ) { - return [`{...${node.text()}}`]; - } - - if (node.kind() !== "object") { - throw new Error(`Unexpected attribute of type "${legacyType(node)}"`); - } - - const attributes: string[] = []; - for (const child of node.children()) { - if (!child.isNamed()) { - continue; - } - - if (child.kind() === "comment") { - continue; - } - - if (child.kind() === "spread_element") { - const argument = spreadArgument(child); - if (!argument) { - throw new Error("Expected spread element argument"); - } - - attributes.push(`{...${argument.text()}}`); - continue; - } - - if (child.kind() === "pair") { - attributes.push(pairToAttribute(child)); - } - } - - return attributes; -} - -function renderExpression(code: string, baseIndent: string): string { - if (!code.includes("\n")) { - return `{${code}}`; - } - - return `{\n${indentAll(code, `${baseIndent} `)}\n${baseIndent}}`; -} - -function renderSimpleChild(node: SgNode, baseIndent: string): { code: string; count: number } { - if (node.kind() === "string") { - const value = stringValue(node); - if (value !== "" && value.trim() === value) { - return { code: encodeJSXTextValue(value), count: 0 }; - } - - return { code: renderExpression(node.text(), baseIndent), count: 0 }; - } - - if (node.kind() === "spread_element") { - const argument = spreadArgument(node); - if (!argument) { - throw new Error("Expected spread child argument"); - } - - return { code: renderExpression(argument.text(), baseIndent), count: 0 }; - } - - if (isReactCreateElementCall(node)) { - const nested = convertSimpleCall(node, baseIndent); - if (!nested) { - return { code: renderExpression(node.text(), baseIndent), count: 0 }; - } - - return { - code: nested.hasComments ? renderExpression(nested.code, baseIndent) : nested.code, - count: nested.count, - }; - } - - return { code: renderExpression(node.text(), baseIndent), count: 0 }; -} - -function convertSimpleCall(call: SgNode, baseIndent: string): Conversion | null { - const args = callArguments(call); - const elementArg = args[0]; - if (!elementArg) { - return null; - } - - const tag = topLevelJsxTag(elementArg); - if (!tag) { - return null; - } - - const propsArg = args[1] ?? null; - const attributes = propsToAttributes(propsArg); - const childIndent = `${baseIndent} `; - const childResults = args.slice(2).map((child) => renderSimpleChild(child, childIndent)); - const attributeSuffix = attributes.length > 0 ? ` ${attributes.join(" ")}` : ""; - - let jsx = `<${tag}${attributeSuffix}`; - let count = 1; - for (const child of childResults) { - count += child.count; - } - - if (childResults.length === 0) { - jsx += " />"; - } else { - const childLines = childResults.map((child) => prefixFirstLine(child.code, childIndent)); - jsx += `>\n${childLines.join("\n")}\n${baseIndent}`; - } - - return { code: jsx, count, hasComments: false }; -} - -function renderAttributeDescriptor(attr: AttrDescriptor, indent: string): string { - const mergedBlocks = [...attr.leadingBlocks]; - if (mergedBlocks.length >= 2 && mergedBlocks[mergedBlocks.length - 1]!.startsWith("//")) { - mergedBlocks[mergedBlocks.length - 2] = `${mergedBlocks[mergedBlocks.length - 2]!}${mergedBlocks[mergedBlocks.length - 1]!}`; - mergedBlocks.pop(); - } - const lines = mergedBlocks.map((block) => `${indent}${block}`); - lines.push(`${indent}${attr.inline}`); - return lines.join("\n"); -} - -function renderExpressionContainer( - core: string, - leadingComments: string[], - trailingComments: string[], - _indent: string, -): string { - const leadingText = leadingComments.join(""); - const trailingText = trailingComments.join(""); - - if (leadingText.length === 0 && trailingText.length === 0 && !core.includes("\n")) { - return `{${core}}`; - } - - if (leadingText.length > 0) { - if (core.includes("\n")) { - return `{\n${core}${trailingText}\n}`; - } - if (!leadingComments.some(isLineComment)) { - return `{${leadingText}${core}${trailingText}}`; - } - return `{${leadingText}\n${core}${trailingText}}`; - } - - if (trailingText.length > 0 && !core.includes("\n")) { - return `{${core}${trailingText}}`; - } - - return `{\n${core}\n}`; -} - -function renderElementCore( - tag: string, - attrs: AttrDescriptor[], - children: { code: string; count: number }[], - baseIndent: string, -): string { - const attrIndent = `${baseIndent} `; - const hasCommentedAttrs = attrs.some((attr) => attr.leadingBlocks.length > 0); - - if (children.length === 0) { - if (attrs.length === 0) { - return `<${tag} />`; - } - - if (hasCommentedAttrs) { - return `<${tag}\n${attrs.map((attr) => renderAttributeDescriptor(attr, attrIndent)).join("\n")}\n${baseIndent}/>`; - } - - return `<${tag} ${attrs.map((attr) => attr.inline).join(" ")} />`; - } - - let opening = `<${tag}>`; - if (attrs.length > 0) { - if (hasCommentedAttrs) { - const attrLines = attrs.map((attr) => renderAttributeDescriptor(attr, attrIndent)); - attrLines[attrLines.length - 1] = `${attrLines[attrLines.length - 1]!}>`; - opening = `<${tag}\n${attrLines.join("\n")}`; - } else { - opening = `<${tag} ${attrs.map((attr) => attr.inline).join(" ")}>`; - } - } - - const childIndent = `${baseIndent} `; - const renderedChildren = children.map((child) => indentAll(child.code, childIndent)); - return `${opening}\n${renderedChildren.join("\n")}\n${baseIndent}`; -} - -function finalizeRenderedElement(rendered: RenderedElement, baseIndent: string): string { - if (!rendered.hasComments) { - return rendered.core; - } - - if (!rendered.core.includes("\n") && rendered.leadingComments.length === 0 && rendered.trailingComments.length > 0) { - return `${rendered.core}${rendered.trailingComments.join("")}`; - } - - if (rendered.leadingComments.length === 0 && rendered.trailingComments.length > 0) { - return `${rendered.core}${rendered.trailingComments.join("")}`; - } - - const innerIndent = `${baseIndent} `; - const lines: string[] = []; - if (rendered.leadingComments.length > 0) { - if (rendered.leadingComments.some(isLineComment)) { - lines.push(...rendered.leadingComments.map((comment) => `${innerIndent}${comment}`)); - lines.push(indentAll(rendered.core, innerIndent)); - } else { - const indentedCore = indentAll(rendered.core, innerIndent); - const coreLines = indentedCore.split("\n"); - coreLines[0] = `${innerIndent}${rendered.leadingComments.join("")}${coreLines[0]!.slice(innerIndent.length)}`; - lines.push(coreLines.join("\n")); - } - } else { - lines.push(indentAll(rendered.core, innerIndent)); - } - - if (rendered.trailingComments.length > 0) { - lines.push(`${innerIndent}${rendered.trailingComments.join("")}`); - } - - return `(\n${lines.join("\n")}\n${baseIndent})`; -} - -function commentedChild( - node: SgNode, - leadingBoundaryComments: SgNode[], - trailingBoundaryComments: SgNode[], - baseIndent: string, - source: string, -): { code: string; count: number } { - let boundaryLeading = [ - ...leadingBoundaryComments.map((comment) => comment.text()), - ...trailingBoundaryComments.map((comment) => comment.text()), - ]; - let trailingInlineComments: string[] = []; - if (isReactCreateElementCall(node) && trailingBoundaryComments.length > 0) { - const split = splitBoundaryCommentsByComma(node, trailingBoundaryComments, source); - boundaryLeading = [ - ...leadingBoundaryComments.map((comment) => comment.text()), - ...split.beforeComma.filter((comment) => isLineComment(comment.text())).map((comment) => comment.text()), - ...split.afterComma.map((comment) => comment.text()), - ]; - trailingInlineComments = split.beforeComma - .filter((comment) => !isLineComment(comment.text())) - .map((comment) => comment.text()); - } - const directLeading = isReactCreateElementCall(node) ? [] : directCommentTexts(node); - const leadingComments = [...boundaryLeading, ...directLeading]; - - if (node.kind() === "string") { - const value = stringValue(node); - if (leadingComments.length === 0 && value !== "" && value.trim() === value) { - return { code: encodeJSXTextValue(value), count: 0 }; - } - - return { - code: renderExpressionContainer(node.text(), leadingComments, [], baseIndent), - count: 0, - }; - } - - if (node.kind() === "spread_element") { - const argument = spreadArgument(node); - if (!argument) { - throw new Error("Expected spread child argument"); - } - - return { - code: renderExpressionContainer(argument.text(), leadingComments, [], baseIndent), - count: 0, - }; - } - - if (isReactCreateElementCall(node)) { - const nested = convertCommentedCall(node, baseIndent, source); - if (!nested) { - return { - code: renderExpressionContainer(node.text(), leadingComments, [], baseIndent), - count: 0, - }; - } - - if (!nested.hasComments && leadingComments.length === 0) { - return { code: nested.core, count: nested.count }; - } - - return { - code: renderExpressionContainer( - nested.core, - [...leadingComments, ...nested.leadingComments], - [ - ...trailingInlineCommentTexts( - trailingBoundaryComments.filter((comment) => !isLineComment(comment.text())), - node.range().end.index, - source, - ), - ...(nested.expressionTrailingComments ?? nested.trailingComments), - ], - baseIndent, - ), - count: nested.count, - }; - } - - return { - code: renderExpressionContainer(node.text(), leadingComments, [], baseIndent), - count: 0, - }; -} - -function convertCommentedCall(call: SgNode, baseIndent: string, source: string): RenderedElement | null { - const layout = argumentLayout(call); - const args = layout.args; - const elementArg = args[0]; - if (!elementArg) { - return null; - } - - const tag = topLevelJsxTag(elementArg); - if (!tag) { - return null; - } - - const tagInfo = tagDescriptor(elementArg); - if (!tagInfo) { - return null; - } - - const callLeadingComments = [ - ...detachedLeadingComments(call).map((comment) => comment.text()), - ...directCommentTexts(call), - ]; - const callee = call.field("function"); - const calleeLeadingComments = [ - ...directCommentTexts(callee), - ...directCommentTexts(callee?.field("object") ?? null), - ...directCommentTexts(callee?.field("property") ?? null), - ]; - - const beforeFirst = layout.boundaryComments[0] ?? []; - const betweenTagAndProps = layout.boundaryComments[1] ?? []; - const propsArg = args[1] ?? null; - const propsIsSpreadLike = propsArg !== null && - (propsArg.kind() === "identifier" || propsArg.kind() === "member_expression" || propsArg.kind() === "call_expression"); - const propsIsNull = propsArg?.kind() === "null"; - const leadingComments = [ - ...callLeadingComments, - ...calleeLeadingComments, - ...beforeFirst.filter((comment) => isLineComment(comment.text())).map((comment) => comment.text()), - ...betweenTagAndProps.filter((comment) => isLineComment(comment.text())).map((comment) => comment.text()), - ]; - const baseTagPropsTrailing = (propsIsSpreadLike || propsIsNull) - ? [] - : betweenTagAndProps.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()); - const trailingComments = !propsArg && elementArg.kind() === "member_expression" - ? [ - ...beforeFirst.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()), - ...baseTagPropsTrailing, - ...tagInfo.identifierComments, - ] - : [ - ...tagInfo.identifierComments, - ...beforeFirst.filter((comment) => !isLineComment(comment.text())).map((comment) => comment.text()), - ...baseTagPropsTrailing, - ]; - - const propsBoundaryAfter = propsArg && args.length === 2 - ? (layout.boundaryComments[2] ?? []) - : []; - const props = propsToDescriptors(propsArg, source); - if (propsArg && (propsArg.kind() === "identifier" || propsArg.kind() === "member_expression" || propsArg.kind() === "call_expression")) { - const spreadAttr = props.attrs[0]; - if (spreadAttr) { - const beforeText = betweenTagAndProps.map((comment) => comment.text()).join(""); - const afterText = propsBoundaryAfter.map((comment) => comment.text()).join(""); - spreadAttr.inline = `{...${beforeText}${propsArg.text()}${afterText}}`; - spreadAttr.hasComments ||= beforeText.length > 0 || afterText.length > 0; - } - } - if (propsIsNull) { - const nullPropComments = [ - ...betweenTagAndProps.map((comment) => comment.text()), - ...propsBoundaryAfter.map((comment) => comment.text()), - ]; - if (nullPropComments.length > 0) { - trailingComments.push(` ${nullPropComments[0]!}`, ...nullPropComments.slice(1)); - } - } - trailingComments.push(...props.trailingElementComments); - - const childIndent = `${baseIndent} `; - const children = args.slice(2).map((child, index) => { - const argIndex = index + 2; - let boundaryBefore = argIndex === 2 ? (layout.boundaryComments[argIndex] ?? []) : []; - if (argIndex === 2 && propsArg) { - const split = splitBoundaryCommentsByComma(propsArg, boundaryBefore, source); - if (propsIsSpreadLike) { - boundaryBefore = [...split.beforeComma, ...split.afterComma]; - } else { - trailingComments.push(...split.beforeComma.map((comment) => comment.text())); - boundaryBefore = split.afterComma; - } - } - const boundaryAfter = layout.boundaryComments[argIndex + 1] ?? []; - return commentedChild(child, boundaryBefore, boundaryAfter, childIndent, source); - }); - - const core = renderElementCore(tagInfo.text, props.attrs, children, ""); - let count = 1; - for (const child of children) { - count += child.count; - } - - return { - core, - count, - leadingComments, - trailingComments, - hasComments: - leadingComments.length > 0 || - trailingComments.length > 0 || - props.attrs.some((attr) => attr.leadingBlocks.length > 0) || - children.some((child) => child.code.includes("\n")), - // Legacy recast printing duplicates a wrapped member-expression element's - // element-level trailing comments, then prints identifier-local comments once - // more. Keep those buckets separate and derive the wrapped form from them. - expressionTrailingComments: - !propsArg && elementArg.kind() === "member_expression" - ? [ - ...trailingComments, - ...trailingComments, - ...tagInfo.identifierComments, - ] - : undefined, - }; -} - -function explicitRequireDisabled(value: unknown): boolean { - return value === false || value === "false"; -} - -const transform: Transform = async (root, options) => { - const rootNode = root.root(); - const source = rootNode.text(); - const edits: Edit[] = []; - const metric = useMetricAtom("create-element-to-jsx-conversions"); - const convertibility = new Map(); - let totalConversions = 0; - - if (!explicitRequireDisabled(options.params?.["explicit-require"]) && !hasReact(rootNode)) { - return null; - } - - const canConvertCall = (call: SgNode): boolean => { - const cached = convertibility.get(call.id()); - if (cached !== undefined) { - return cached; - } - - const args = callArguments(call); - const elementArg = args[0]; - const convertible = elementArg !== undefined && topLevelJsxTag(elementArg) !== null; - convertibility.set(call.id(), convertible); - return convertible; - }; - - const createElementCalls = rootNode.findAll({ rule: { kind: "call_expression" } }) - .filter(isReactCreateElementCall); - - for (const call of createElementCalls) { - const coveredByAncestor = call.ancestors().some((ancestor) => - isReactCreateElementCall(ancestor) && canConvertCall(ancestor) - ); - if (coveredByAncestor) { - continue; - } - - const baseIndent = lineIndent(source, call.range().start.index); - const conversion = ownCommentNodes(call).length > 0 - ? convertCommentedCall(call, baseIndent, source) - : convertSimpleCall(call, baseIndent); - if (!conversion) { - continue; - } - - const code = "core" in conversion ? finalizeRenderedElement(conversion, baseIndent) : conversion.code; - if ("core" in conversion) { - const detachedComments = detachedLeadingComments(call); - const replacementStart = detachedComments.length > 0 - ? Math.min(...detachedComments.map((comment) => comment.range().start.index)) - : call.range().start.index; - edits.push({ - startPos: replacementStart, - endPos: call.range().end.index, - insertedText: code, - }); - } else { - edits.push(call.replace(code)); - } - totalConversions += conversion.count; - } - - if (edits.length === 0) { - return null; - } - - metric.increment({ - file: metricFile(root.filename()), - }, totalConversions); - - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/create-element-to-jsx/scripts/differential-tests.mjs b/codemods/create-element-to-jsx/scripts/differential-tests.mjs deleted file mode 100644 index bfd1d5b..0000000 --- a/codemods/create-element-to-jsx/scripts/differential-tests.mjs +++ /dev/null @@ -1,69 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __filename = fileURLToPath(import.meta.url); -const packageDir = path.dirname(path.dirname(__filename)); -const workflowPath = path.join(packageDir, "workflow.yaml"); -const testsDir = path.join(packageDir, "tests"); - -function readFixture(name, file) { - return readFileSync(path.join(testsDir, name, file), "utf8").trim(); -} - -function runJssg(source) { - const dir = mkdtempSync(path.join(tmpdir(), "ce2jsx-jssg-")); - const inputPath = path.join(dir, "input.tsx"); - writeFileSync(inputPath, source, "utf8"); - writeFileSync(path.join(dir, ".gitignore"), "", "utf8"); - - try { - spawnSync("git", ["init", "-q"], { cwd: dir }); - spawnSync("git", ["add", "input.tsx", ".gitignore"], { cwd: dir }); - spawnSync("git", ["commit", "-qm", "init"], { cwd: dir }); - - const result = spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", - "workflow", - "run", - "-w", - workflowPath, - "--target", - dir, - "--allow-dirty", - ], - { - cwd: packageDir, - encoding: "utf8", - timeout: 20000, - }, - ); - assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); - return readFileSync(inputPath, "utf8").trim(); - } finally { - rmSync(dir, { recursive: true, force: true }); - } -} - -const parityCases = [ - "member-simple-comments", - "spread-prop-comments", - "member-child-comments-alt", - "comment-only-props-string-child", -]; - -for (const name of parityCases) { - test(`matches checked-in parity fixture for ${name}`, () => { - const input = readFixture(name, "input.tsx"); - const expected = readFixture(name, "expected.tsx"); - const jssgOutput = runJssg(input); - assert.strictEqual(jssgOutput, expected); - }); -} diff --git a/codemods/create-element-to-jsx/scripts/error-tests.mjs b/codemods/create-element-to-jsx/scripts/error-tests.mjs deleted file mode 100644 index 5480d1f..0000000 --- a/codemods/create-element-to-jsx/scripts/error-tests.mjs +++ /dev/null @@ -1,47 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __filename = fileURLToPath(import.meta.url); -const packageDir = path.dirname(path.dirname(__filename)); -const workflowPath = path.join(packageDir, "workflow.yaml"); - -function runCodemod(input) { - const target = mkdtempSync(path.join(tmpdir(), "create-element-to-jsx-")); - const filePath = path.join(target, "input.tsx"); - writeFileSync(filePath, input, "utf8"); - - try { - return spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", - "workflow", - "run", - "-w", - workflowPath, - "--target", - target, - "--allow-dirty", - ], - { - cwd: packageDir, - encoding: "utf8", - }, - ); - } finally { - rmSync(target, { recursive: true, force: true }); - } -} - -test("throws on unsupported props literal", () => { - const result = runCodemod("var React = require('react/addons');\nReact.createElement('foo', 1);\n"); - const combined = `${result.stdout}\n${result.stderr}`; - assert.match(combined, /Unexpected attribute of type .*Literal/); - assert.match(combined, /Failed to execute codemod|InitializationFailed/); -}); diff --git a/codemods/create-element-to-jsx/tests/allow-member-expression/metrics.json b/codemods/create-element-to-jsx/tests/allow-member-expression/metrics.json deleted file mode 100644 index d3b422e..0000000 --- a/codemods/create-element-to-jsx/tests/allow-member-expression/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/allow-member-expression/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/arg-spread/metrics.json b/codemods/create-element-to-jsx/tests/arg-spread/metrics.json deleted file mode 100644 index 3c3571e..0000000 --- a/codemods/create-element-to-jsx/tests/arg-spread/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/arg-spread/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/call-as-children/metrics.json b/codemods/create-element-to-jsx/tests/call-as-children/metrics.json deleted file mode 100644 index 9b83be3..0000000 --- a/codemods/create-element-to-jsx/tests/call-as-children/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/call-as-children/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/call-expression-as-prop/metrics.json b/codemods/create-element-to-jsx/tests/call-expression-as-prop/metrics.json deleted file mode 100644 index 1901fdb..0000000 --- a/codemods/create-element-to-jsx/tests/call-expression-as-prop/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/call-expression-as-prop/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/children-literal/metrics.json b/codemods/create-element-to-jsx/tests/children-literal/metrics.json deleted file mode 100644 index bff6a4f..0000000 --- a/codemods/create-element-to-jsx/tests/children-literal/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/children-literal/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/children-map/metrics.json b/codemods/create-element-to-jsx/tests/children-map/metrics.json deleted file mode 100644 index 2b46493..0000000 --- a/codemods/create-element-to-jsx/tests/children-map/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/children-map/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/children-mixed-empty-string/metrics.json b/codemods/create-element-to-jsx/tests/children-mixed-empty-string/metrics.json deleted file mode 100644 index 5bf7bb6..0000000 --- a/codemods/create-element-to-jsx/tests/children-mixed-empty-string/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/children-mixed-empty-string/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/children/metrics.json b/codemods/create-element-to-jsx/tests/children/metrics.json deleted file mode 100644 index 3737f1c..0000000 --- a/codemods/create-element-to-jsx/tests/children/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/children/input.tsx" - }, - "count": 3 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/comment-only-props-string-child/expected.tsx b/codemods/create-element-to-jsx/tests/comment-only-props-string-child/expected.tsx deleted file mode 100644 index 7b85a75..0000000 --- a/codemods/create-element-to-jsx/tests/comment-only-props-string-child/expected.tsx +++ /dev/null @@ -1,4 +0,0 @@ -var React = require('react'); - - x -/*P*/; diff --git a/codemods/create-element-to-jsx/tests/comment-only-props-string-child/input.tsx b/codemods/create-element-to-jsx/tests/comment-only-props-string-child/input.tsx deleted file mode 100644 index 9f279a8..0000000 --- a/codemods/create-element-to-jsx/tests/comment-only-props-string-child/input.tsx +++ /dev/null @@ -1,2 +0,0 @@ -var React = require('react'); -React.createElement(Foo, {/*P*/}, 'x'); diff --git a/codemods/create-element-to-jsx/tests/comment-only-props-string-child/metrics.json b/codemods/create-element-to-jsx/tests/comment-only-props-string-child/metrics.json deleted file mode 100644 index df02d4e..0000000 --- a/codemods/create-element-to-jsx/tests/comment-only-props-string-child/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/comment-only-props-string-child/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/deep-nesting/metrics.json b/codemods/create-element-to-jsx/tests/deep-nesting/metrics.json deleted file mode 100644 index ab5be8b..0000000 --- a/codemods/create-element-to-jsx/tests/deep-nesting/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/deep-nesting/input.tsx" - }, - "count": 7 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/element-comment-positioning/metrics.json b/codemods/create-element-to-jsx/tests/element-comment-positioning/metrics.json deleted file mode 100644 index 56d512e..0000000 --- a/codemods/create-element-to-jsx/tests/element-comment-positioning/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/element-comment-positioning/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/escaped-string/metrics.json b/codemods/create-element-to-jsx/tests/escaped-string/metrics.json deleted file mode 100644 index a4d2834..0000000 --- a/codemods/create-element-to-jsx/tests/escaped-string/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/escaped-string/input.tsx" - }, - "count": 5 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/gt-lt-entities/metrics.json b/codemods/create-element-to-jsx/tests/gt-lt-entities/metrics.json deleted file mode 100644 index 359e1f6..0000000 --- a/codemods/create-element-to-jsx/tests/gt-lt-entities/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/gt-lt-entities/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/metrics.json b/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/metrics.json deleted file mode 100644 index d414e38..0000000 --- a/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/ignore-bad-capitalization/input.tsx" - }, - "count": 5 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/literal-prop/metrics.json b/codemods/create-element-to-jsx/tests/literal-prop/metrics.json deleted file mode 100644 index d9ec56c..0000000 --- a/codemods/create-element-to-jsx/tests/literal-prop/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/literal-prop/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/literal-spacing/metrics.json b/codemods/create-element-to-jsx/tests/literal-spacing/metrics.json deleted file mode 100644 index 5b0df95..0000000 --- a/codemods/create-element-to-jsx/tests/literal-spacing/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/literal-spacing/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/member-child-comments-alt/expected.tsx b/codemods/create-element-to-jsx/tests/member-child-comments-alt/expected.tsx deleted file mode 100644 index 4d793b3..0000000 --- a/codemods/create-element-to-jsx/tests/member-child-comments-alt/expected.tsx +++ /dev/null @@ -1,4 +0,0 @@ -var React = require('react'); -
    - { /*E*//*A*//*D*//*B*//*C*//*A*//*D*//*B*//*C*//*B*//*C*/} -
    ; diff --git a/codemods/create-element-to-jsx/tests/member-child-comments-alt/input.tsx b/codemods/create-element-to-jsx/tests/member-child-comments-alt/input.tsx deleted file mode 100644 index 067f04a..0000000 --- a/codemods/create-element-to-jsx/tests/member-child-comments-alt/input.tsx +++ /dev/null @@ -1,2 +0,0 @@ -var React = require('react'); -React.createElement('div', null, React.createElement(/*A*/Foo/*B*/./*C*/Bar/*D*/) /*E*/); diff --git a/codemods/create-element-to-jsx/tests/member-child-comments-alt/metrics.json b/codemods/create-element-to-jsx/tests/member-child-comments-alt/metrics.json deleted file mode 100644 index 990a6a2..0000000 --- a/codemods/create-element-to-jsx/tests/member-child-comments-alt/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/member-child-comments-alt/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/member-expression-as-prop/metrics.json b/codemods/create-element-to-jsx/tests/member-expression-as-prop/metrics.json deleted file mode 100644 index 76d7ff6..0000000 --- a/codemods/create-element-to-jsx/tests/member-expression-as-prop/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/member-expression-as-prop/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/member-simple-comments/expected.tsx b/codemods/create-element-to-jsx/tests/member-simple-comments/expected.tsx deleted file mode 100644 index db7c05f..0000000 --- a/codemods/create-element-to-jsx/tests/member-simple-comments/expected.tsx +++ /dev/null @@ -1,2 +0,0 @@ -var React = require('react'); -/*A*//*D*//*B*//*C*/; diff --git a/codemods/create-element-to-jsx/tests/member-simple-comments/input.tsx b/codemods/create-element-to-jsx/tests/member-simple-comments/input.tsx deleted file mode 100644 index f3c8928..0000000 --- a/codemods/create-element-to-jsx/tests/member-simple-comments/input.tsx +++ /dev/null @@ -1,2 +0,0 @@ -var React = require('react'); -React.createElement(/*A*/Foo/*B*/./*C*/Bar/*D*/); diff --git a/codemods/create-element-to-jsx/tests/member-simple-comments/metrics.json b/codemods/create-element-to-jsx/tests/member-simple-comments/metrics.json deleted file mode 100644 index f78cdca..0000000 --- a/codemods/create-element-to-jsx/tests/member-simple-comments/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/member-simple-comments/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/no-props-arg/metrics.json b/codemods/create-element-to-jsx/tests/no-props-arg/metrics.json deleted file mode 100644 index 0eed063..0000000 --- a/codemods/create-element-to-jsx/tests/no-props-arg/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/no-props-arg/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/expected.tsx b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/expected.tsx deleted file mode 100644 index 8fb7b0c..0000000 --- a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -var React = require('foo'); - -; diff --git a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/input.tsx b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/input.tsx deleted file mode 100644 index 789178e..0000000 --- a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/input.tsx +++ /dev/null @@ -1,3 +0,0 @@ -var React = require('foo'); - -React.createElement(Foo, null); diff --git a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/metrics.json b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/metrics.json deleted file mode 100644 index cccf0c4..0000000 --- a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/no-react-valid-explicit-require/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/test.config.json b/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/test.config.json deleted file mode 100644 index 4858435..0000000 --- a/codemods/create-element-to-jsx/tests/no-react-valid-explicit-require/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "explicit-require": false - } -} diff --git a/codemods/create-element-to-jsx/tests/no-react-valid/expected.tsx b/codemods/create-element-to-jsx/tests/no-react-valid/expected.tsx deleted file mode 100644 index 789178e..0000000 --- a/codemods/create-element-to-jsx/tests/no-react-valid/expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -var React = require('foo'); - -React.createElement(Foo, null); diff --git a/codemods/create-element-to-jsx/tests/no-react-valid/input.tsx b/codemods/create-element-to-jsx/tests/no-react-valid/input.tsx deleted file mode 100644 index 789178e..0000000 --- a/codemods/create-element-to-jsx/tests/no-react-valid/input.tsx +++ /dev/null @@ -1,3 +0,0 @@ -var React = require('foo'); - -React.createElement(Foo, null); diff --git a/codemods/create-element-to-jsx/tests/no-react/input.tsx b/codemods/create-element-to-jsx/tests/no-react/input.tsx deleted file mode 100644 index aa81e70..0000000 --- a/codemods/create-element-to-jsx/tests/no-react/input.tsx +++ /dev/null @@ -1,3 +0,0 @@ -var React = require('foo'); - -React.createElement(Foo, 'la'); diff --git a/codemods/create-element-to-jsx/tests/object-assign/metrics.json b/codemods/create-element-to-jsx/tests/object-assign/metrics.json deleted file mode 100644 index 09a364c..0000000 --- a/codemods/create-element-to-jsx/tests/object-assign/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/object-assign/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/preserve-comments/metrics.json b/codemods/create-element-to-jsx/tests/preserve-comments/metrics.json deleted file mode 100644 index dfde4e1..0000000 --- a/codemods/create-element-to-jsx/tests/preserve-comments/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/preserve-comments/input.tsx" - }, - "count": 8 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/props-array/metrics.json b/codemods/create-element-to-jsx/tests/props-array/metrics.json deleted file mode 100644 index 666ddb5..0000000 --- a/codemods/create-element-to-jsx/tests/props-array/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/props-array/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/props-boolean/metrics.json b/codemods/create-element-to-jsx/tests/props-boolean/metrics.json deleted file mode 100644 index 5cf9562..0000000 --- a/codemods/create-element-to-jsx/tests/props-boolean/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/props-boolean/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/props/metrics.json b/codemods/create-element-to-jsx/tests/props/metrics.json deleted file mode 100644 index e85cae5..0000000 --- a/codemods/create-element-to-jsx/tests/props/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/props/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/react-spread/metrics.json b/codemods/create-element-to-jsx/tests/react-spread/metrics.json deleted file mode 100644 index af33fc4..0000000 --- a/codemods/create-element-to-jsx/tests/react-spread/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/react-spread/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/single-element/metrics.json b/codemods/create-element-to-jsx/tests/single-element/metrics.json deleted file mode 100644 index 3dc19f2..0000000 --- a/codemods/create-element-to-jsx/tests/single-element/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/single-element/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/spread-prop-comments/expected.tsx b/codemods/create-element-to-jsx/tests/spread-prop-comments/expected.tsx deleted file mode 100644 index 3a8cec3..0000000 --- a/codemods/create-element-to-jsx/tests/spread-prop-comments/expected.tsx +++ /dev/null @@ -1,4 +0,0 @@ -var React = require('react'); -
    - {/*B*/'x'} -
    ; diff --git a/codemods/create-element-to-jsx/tests/spread-prop-comments/input.tsx b/codemods/create-element-to-jsx/tests/spread-prop-comments/input.tsx deleted file mode 100644 index 95b2b96..0000000 --- a/codemods/create-element-to-jsx/tests/spread-prop-comments/input.tsx +++ /dev/null @@ -1,2 +0,0 @@ -var React = require('react'); -React.createElement('div', /*A*/getProps()/*B*/, 'x'); diff --git a/codemods/create-element-to-jsx/tests/spread-prop-comments/metrics.json b/codemods/create-element-to-jsx/tests/spread-prop-comments/metrics.json deleted file mode 100644 index e5b890a..0000000 --- a/codemods/create-element-to-jsx/tests/spread-prop-comments/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/spread-prop-comments/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/spread-props/metrics.json b/codemods/create-element-to-jsx/tests/spread-props/metrics.json deleted file mode 100644 index 09ceb40..0000000 --- a/codemods/create-element-to-jsx/tests/spread-props/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/spread-props/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/create-element-to-jsx/tests/spread/metrics.json b/codemods/create-element-to-jsx/tests/spread/metrics.json deleted file mode 100644 index 5fd18ec..0000000 --- a/codemods/create-element-to-jsx/tests/spread/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "create-element-to-jsx-conversions": [ - { - "cardinality": { - "file": "tests/spread/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/error-boundaries/README.md b/codemods/error-boundaries/README.md deleted file mode 100644 index 7f185ba..0000000 --- a/codemods/error-boundaries/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/error-boundaries - -Rename `unstable_handleError` to `componentDidCatch` in React class components and `createClass` component definitions. - -## Usage - -```bash -codemod run @react/error-boundaries -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/error-boundaries/codemod.yaml b/codemods/error-boundaries/codemod.yaml deleted file mode 100644 index bb02200..0000000 --- a/codemods/error-boundaries/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/error-boundaries" -version: "0.1.0" -description: "Rename unstable_handleError to componentDidCatch" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/error-boundaries/package.json b/codemods/error-boundaries/package.json deleted file mode 100644 index 7c0d19c..0000000 --- a/codemods/error-boundaries/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@react/error-boundaries", - "version": "0.1.0", - "description": "Rename unstable_handleError to componentDidCatch", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "typescript": "latest", - "@types/node": "latest" - } -} diff --git a/codemods/error-boundaries/scripts/codemod.ts b/codemods/error-boundaries/scripts/codemod.ts deleted file mode 100644 index 79c8434..0000000 --- a/codemods/error-boundaries/scripts/codemod.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const edits: Edit[] = []; - - const renameMetric = useMetricAtom("error-boundary-renames"); - - const classLifecycleNames = rootNode.findAll({ - rule: { - kind: "property_identifier", - regex: "^unstable_handleError$", - inside: { - any: [ - { - kind: "method_definition", - inside: { kind: "class_body" }, - }, - { - kind: "public_field_definition", - inside: { kind: "class_body" }, - }, - ], - }, - }, - }); - - const createClassCalls = rootNode.findAll({ - rule: { - kind: "call_expression", - has: { - field: "function", - kind: "identifier", - regex: "^(createReactClass|createClass)$", - }, - }, - }); - - const createClassLifecycleNames: SgNode[] = []; - for (const call of createClassCalls) { - const args = call.field("arguments"); - if (!args) continue; - const argList = args.children().filter( - (c) => c.kind() !== "(" && c.kind() !== ")" && c.kind() !== ",", - ); - const configObj = argList[0]; - if (configObj) { - createClassLifecycleNames.push( - ...configObj.findAll({ - rule: { - kind: "property_identifier" as const, - regex: "^unstable_handleError$", - }, - }), - ); - } - } - - for (const node of classLifecycleNames) { - edits.push(node.replace("componentDidCatch")); - } - for (const node of createClassLifecycleNames) { - edits.push(node.replace("componentDidCatch")); - } - - if (edits.length > 0) { - renameMetric.increment( - { - file: metricFile(root.filename()), - }, - edits.length, - ); - } - - if (edits.length === 0) { - return null; - } - - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/error-boundaries/tests/class-component/metrics.json b/codemods/error-boundaries/tests/class-component/metrics.json deleted file mode 100644 index ce4a6fe..0000000 --- a/codemods/error-boundaries/tests/class-component/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error-boundary-renames": [ - { - "cardinality": { - "file": "tests/class-component/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/error-boundaries/tests/create-class-component/metrics.json b/codemods/error-boundaries/tests/create-class-component/metrics.json deleted file mode 100644 index 21136e9..0000000 --- a/codemods/error-boundaries/tests/create-class-component/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error-boundary-renames": [ - { - "cardinality": { - "file": "tests/create-class-component/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/find-dom-node/README.md b/codemods/find-dom-node/README.md deleted file mode 100644 index 2ffc54d..0000000 --- a/codemods/find-dom-node/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# @react/find-dom-node - -Replace `.getDOMNode()` calls with `React.findDOMNode(...)`. - -## Usage - -```bash -codemod run @react/find-dom-node -``` - -## Options - -- `explicit-require`: when `false`, run even if no React import/require is present. Default: `true`. - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/find-dom-node/codemod.yaml b/codemods/find-dom-node/codemod.yaml deleted file mode 100644 index 51f9a8e..0000000 --- a/codemods/find-dom-node/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/find-dom-node" -version: "0.1.0" -description: "Replace getDOMNode() with React.findDOMNode()" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/find-dom-node/package.json b/codemods/find-dom-node/package.json deleted file mode 100644 index de25953..0000000 --- a/codemods/find-dom-node/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@react/find-dom-node", - "version": "0.1.0", - "description": "Replace getDOMNode() with React.findDOMNode()", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "typescript": "latest", - "@types/node": "latest" - }, - "dependencies": { - "@jssg/utils": "^0.0.2" - } -} diff --git a/codemods/find-dom-node/scripts/codemod.ts b/codemods/find-dom-node/scripts/codemod.ts deleted file mode 100644 index b98dc0a..0000000 --- a/codemods/find-dom-node/scripts/codemod.ts +++ /dev/null @@ -1,175 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -function stringValue(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function callArguments(call: SgNode): SgNode[] { - const args = call.field("arguments"); - if (!args) return []; - return args.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function requireSource(call: SgNode): string | null { - const callee = call.field("function"); - if (!callee || callee.kind() !== "identifier" || callee.text() !== "require") return null; - const firstArg = callArguments(call)[0]; - return firstArg?.kind() === "string" ? stringValue(firstArg) : null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? stringValue(source) : null; -} - -function hasReact(rootNode: SgNode): boolean { - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (REACT_MODULES.has(importSource(importNode) ?? "")) return true; - } - - for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - if (REACT_MODULES.has(requireSource(call) ?? "")) return true; - } - - return false; -} - -function explicitRequireDisabled(value: unknown): boolean { - return value === false || value === "false"; -} - -function isReactCreateClassCall(node: SgNode | null): node is SgNode { - if (!node || node.kind() !== "call_expression") return false; - const callee = node.field("function"); - return callee?.kind() === "member_expression" && - callee.field("object")?.kind() === "identifier" && - callee.field("object")?.text() === "React" && - callee.field("property")?.kind() === "property_identifier" && - callee.field("property")?.text() === "createClass"; -} - -function isModuleExportsMember(node: SgNode | null): boolean { - return node?.kind() === "member_expression" && - node.field("object")?.kind() === "identifier" && - node.field("object")?.text() === "module" && - node.field("property")?.kind() === "property_identifier" && - node.field("property")?.text() === "exports"; -} - -function createClassContainers(rootNode: SgNode): SgNode[] { - const containers: SgNode[] = []; - - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - if (isReactCreateClassCall(declarator.field("value"))) containers.push(declarator); - } - - for (const assignment of rootNode.findAll({ rule: { kind: "assignment_expression" } })) { - if (isModuleExportsMember(assignment.field("left")) && isReactCreateClassCall(assignment.field("right"))) { - containers.push(assignment); - } - } - - return containers; -} - -function isThisRefsBase(node: SgNode | null): boolean { - return node?.kind() === "member_expression" && - node.field("object")?.kind() === "this" && - node.field("property")?.kind() === "property_identifier" && - node.field("property")?.text() === "refs"; -} - -function accessObject(node: SgNode | null): SgNode | null { - if (!node) return null; - if (node.kind() === "member_expression" || node.kind() === "subscript_expression") { - return node.field("object"); - } - return null; -} - -function isThisRefsChain(node: SgNode | null): boolean { - if (!node) return false; - const object = accessObject(node); - if (!object) return false; - if (isThisRefsBase(object)) return true; - return isThisRefsChain(object); -} - -function isDirectRefsAlias(node: SgNode | null): boolean { - if (!node) return false; - const object = accessObject(node); - return isThisRefsBase(object); -} - -function isGetDOMNodeCall(call: SgNode, objectMatcher: (node: SgNode | null) => boolean): boolean { - const callee = call.field("function"); - return callee?.kind() === "member_expression" && - callee.field("property")?.kind() === "property_identifier" && - callee.field("property")?.text() === "getDOMNode" && - objectMatcher(callee.field("object")); -} - -const transform: Transform = async (root, options) => { - const rootNode = root.root(); - if (!explicitRequireDisabled(options.params?.["explicit-require"]) && !hasReact(rootNode)) { - return null; - } - - const metric = useMetricAtom("find-dom-node-replacements"); - const edits: Edit[] = []; - let replacements = 0; - - for (const container of createClassContainers(rootNode)) { - for (const call of container.findAll({ rule: { kind: "call_expression" } })) { - if (isGetDOMNodeCall(call, (node) => node?.kind() === "this")) { - edits.push(call.replace("React.findDOMNode(this)")); - replacements++; - continue; - } - - if (isGetDOMNodeCall(call, isThisRefsChain)) { - const target = call.field("function")?.field("object"); - if (!target) continue; - edits.push(call.replace(`React.findDOMNode(${target.text()})`)); - replacements++; - } - } - - for (const declarator of container.findAll({ rule: { kind: "variable_declarator" } })) { - const name = declarator.field("name"); - const value = declarator.field("value"); - if (name?.kind() !== "identifier" || !isDirectRefsAlias(value)) continue; - - const functionExpression = declarator.ancestors().find((ancestor) => ancestor.kind() === "function_expression"); - if (!functionExpression) continue; - - for (const call of functionExpression.findAll({ rule: { kind: "call_expression" } })) { - if (!isGetDOMNodeCall(call, (node) => node?.kind() === "identifier" && node.text() === name.text())) { - continue; - } - - edits.push(call.replace(`React.findDOMNode(${name.text()})`)); - replacements++; - } - } - } - - if (edits.length === 0) return null; - - metric.increment({ file: metricFile(root.filename()) }, replacements); - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/find-dom-node/tests/computed-ref-alias/expected.tsx b/codemods/find-dom-node/tests/computed-ref-alias/expected.tsx deleted file mode 100644 index b66b1ac..0000000 --- a/codemods/find-dom-node/tests/computed-ref-alias/expected.tsx +++ /dev/null @@ -1,8 +0,0 @@ -var React = require('React'); -var C = React.createClass({ - render: function() { - var ref = 'foo'; - var thing = this.refs[ref]; - return React.findDOMNode(thing); - } -}); diff --git a/codemods/find-dom-node/tests/computed-ref-alias/input.tsx b/codemods/find-dom-node/tests/computed-ref-alias/input.tsx deleted file mode 100644 index cd5129a..0000000 --- a/codemods/find-dom-node/tests/computed-ref-alias/input.tsx +++ /dev/null @@ -1,8 +0,0 @@ -var React = require('React'); -var C = React.createClass({ - render: function() { - var ref = 'foo'; - var thing = this.refs[ref]; - return thing.getDOMNode(); - } -}); diff --git a/codemods/find-dom-node/tests/computed-ref-alias/metrics.json b/codemods/find-dom-node/tests/computed-ref-alias/metrics.json deleted file mode 100644 index fe6a6e0..0000000 --- a/codemods/find-dom-node/tests/computed-ref-alias/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/computed-ref-alias/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/deep-refs-chain/expected.tsx b/codemods/find-dom-node/tests/deep-refs-chain/expected.tsx deleted file mode 100644 index dee893c..0000000 --- a/codemods/find-dom-node/tests/deep-refs-chain/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ -var React = require('React'); -var C = React.createClass({ - render: function() { - return React.findDOMNode(this.refs.main.refs.list); - } -}); diff --git a/codemods/find-dom-node/tests/deep-refs-chain/input.tsx b/codemods/find-dom-node/tests/deep-refs-chain/input.tsx deleted file mode 100644 index 7f94962..0000000 --- a/codemods/find-dom-node/tests/deep-refs-chain/input.tsx +++ /dev/null @@ -1,6 +0,0 @@ -var React = require('React'); -var C = React.createClass({ - render: function() { - return this.refs.main.refs.list.getDOMNode(); - } -}); diff --git a/codemods/find-dom-node/tests/deep-refs-chain/metrics.json b/codemods/find-dom-node/tests/deep-refs-chain/metrics.json deleted file mode 100644 index ae90bb8..0000000 --- a/codemods/find-dom-node/tests/deep-refs-chain/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/deep-refs-chain/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx b/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx deleted file mode 100644 index 464aee7..0000000 --- a/codemods/find-dom-node/tests/es6-class-no-change/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -class C extends React.Component { - render() { - return this.getDOMNode(); - } -} diff --git a/codemods/find-dom-node/tests/es6-class-no-change/input.tsx b/codemods/find-dom-node/tests/es6-class-no-change/input.tsx deleted file mode 100644 index 464aee7..0000000 --- a/codemods/find-dom-node/tests/es6-class-no-change/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -class C extends React.Component { - render() { - return this.getDOMNode(); - } -} diff --git a/codemods/find-dom-node/tests/export-default-no-change/expected.tsx b/codemods/find-dom-node/tests/export-default-no-change/expected.tsx deleted file mode 100644 index 97fa6eb..0000000 --- a/codemods/find-dom-node/tests/export-default-no-change/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -export default React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/find-dom-node/tests/export-default-no-change/input.tsx b/codemods/find-dom-node/tests/export-default-no-change/input.tsx deleted file mode 100644 index 97fa6eb..0000000 --- a/codemods/find-dom-node/tests/export-default-no-change/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -export default React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/find-dom-node/tests/findDOMNode/metrics.json b/codemods/find-dom-node/tests/findDOMNode/metrics.json deleted file mode 100644 index 960ef58..0000000 --- a/codemods/find-dom-node/tests/findDOMNode/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/findDOMNode/input.tsx" - }, - "count": 5 - } - ] -} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/helper-no-change/expected.tsx b/codemods/find-dom-node/tests/helper-no-change/expected.tsx deleted file mode 100644 index 281d7fa..0000000 --- a/codemods/find-dom-node/tests/helper-no-change/expected.tsx +++ /dev/null @@ -1,2 +0,0 @@ -const helper = foo(); -helper.getDOMNode(); diff --git a/codemods/find-dom-node/tests/helper-no-change/input.tsx b/codemods/find-dom-node/tests/helper-no-change/input.tsx deleted file mode 100644 index 281d7fa..0000000 --- a/codemods/find-dom-node/tests/helper-no-change/input.tsx +++ /dev/null @@ -1,2 +0,0 @@ -const helper = foo(); -helper.getDOMNode(); diff --git a/codemods/find-dom-node/tests/module-exports/expected.tsx b/codemods/find-dom-node/tests/module-exports/expected.tsx deleted file mode 100644 index cab9f4c..0000000 --- a/codemods/find-dom-node/tests/module-exports/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -module.exports = React.createClass({ - render: function() { - return React.findDOMNode(this); - }, -}); diff --git a/codemods/find-dom-node/tests/module-exports/input.tsx b/codemods/find-dom-node/tests/module-exports/input.tsx deleted file mode 100644 index 9658cf5..0000000 --- a/codemods/find-dom-node/tests/module-exports/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var React = require('React'); - -module.exports = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/find-dom-node/tests/module-exports/metrics.json b/codemods/find-dom-node/tests/module-exports/metrics.json deleted file mode 100644 index 42a4df1..0000000 --- a/codemods/find-dom-node/tests/module-exports/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/module-exports/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx deleted file mode 100644 index dd2e29f..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/expected.tsx +++ /dev/null @@ -1,5 +0,0 @@ -var Composer = React.createClass({ - render: function() { - return React.findDOMNode(this); - }, -}); diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx deleted file mode 100644 index 8b8bbf5..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -var Composer = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json deleted file mode 100644 index 6b828be..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "find-dom-node-replacements": [ - { - "cardinality": { - "file": "tests/no-react-create-class-explicit-require/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json b/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json deleted file mode 100644 index 4858435..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class-explicit-require/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "explicit-require": false - } -} diff --git a/codemods/find-dom-node/tests/no-react-create-class/expected.tsx b/codemods/find-dom-node/tests/no-react-create-class/expected.tsx deleted file mode 100644 index 8b8bbf5..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class/expected.tsx +++ /dev/null @@ -1,5 +0,0 @@ -var Composer = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/find-dom-node/tests/no-react-create-class/input.tsx b/codemods/find-dom-node/tests/no-react-create-class/input.tsx deleted file mode 100644 index 8b8bbf5..0000000 --- a/codemods/find-dom-node/tests/no-react-create-class/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -var Composer = React.createClass({ - render: function() { - return this.getDOMNode(); - }, -}); diff --git a/codemods/jssg/react-proptypes-to-prop-types/README.md b/codemods/jssg/react-proptypes-to-prop-types/README.md new file mode 100644 index 0000000..5ac582a --- /dev/null +++ b/codemods/jssg/react-proptypes-to-prop-types/README.md @@ -0,0 +1,20 @@ +# react-proptypes-to-prop-types + +Replace legacy React `PropTypes` usage with the `prop-types` package across imports, requires, aliases, and destructuring patterns. + +## Run Locally + +Run this command from this directory: + +```bash +pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +``` + +To override the `prop-types` module specifier, pass `--param module-name=`. + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/react-proptypes-to-prop-types/codemod.yaml b/codemods/jssg/react-proptypes-to-prop-types/codemod.yaml similarity index 100% rename from codemods/react-proptypes-to-prop-types/codemod.yaml rename to codemods/jssg/react-proptypes-to-prop-types/codemod.yaml diff --git a/codemods/react-proptypes-to-prop-types/package.json b/codemods/jssg/react-proptypes-to-prop-types/package.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/package.json rename to codemods/jssg/react-proptypes-to-prop-types/package.json diff --git a/codemods/react-proptypes-to-prop-types/scripts/codemod.ts b/codemods/jssg/react-proptypes-to-prop-types/scripts/codemod.ts similarity index 100% rename from codemods/react-proptypes-to-prop-types/scripts/codemod.ts rename to codemods/jssg/react-proptypes-to-prop-types/scripts/codemod.ts diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-import/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-import/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-import/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-import/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-import/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-import/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-import/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-import/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-import/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-import/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-as-import/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-import/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-import/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-import/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-import/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-import/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-named-import/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-named-import/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-require/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-require/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-require/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-require/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-require/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-require/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/already-migrated-require/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-require/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/already-migrated-require/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/already-migrated-require/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/assigned-from-react-var/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/assigned-from-react-var/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/assigned-from-react-var/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/assigned-from-react-var/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/assigned-from-react-var/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/assigned-from-react-var/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/assigned-from-react-var/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/assigned-to-var-with-different-name/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/default-and-named-import/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/default-and-named-import/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/default-and-named-import/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/default-and-named-import/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/default-and-named-import/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/default-import/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/default-import/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/default-import/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/default-import/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/default-import/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/default-import/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/default-import/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/default-import/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/default-import/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/default-import/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/default-import/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/destructured-proptypes-import/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/destructured-proptypes-import/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/destructured-proptypes-import/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/destructured-proptypes-import/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/destructured-proptypes-import/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/import-alias/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/import-alias/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/import-alias/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/import-alias/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/import-alias/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/import-alias/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/import-alias/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/import-flow-type-with-require/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/import-flow-type-with-require/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/import-flow-type-with-require/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/import-flow-type-with-require/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require-2/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/mixed-import-and-require/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/mixed-import-and-require/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/module-name/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/module-name/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/module-name/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/module-name/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/module-name/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/module-name/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/module-name/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/module-name/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/module-name/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/module-name/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/module-name/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/module-name/test.config.json b/codemods/jssg/react-proptypes-to-prop-types/tests/module-name/test.config.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/module-name/test.config.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/module-name/test.config.json diff --git a/codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/named-parameters/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/named-parameters/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/named-parameters/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/named-parameters/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/named-parameters/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/nested-destructured-proptypes-import/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/no-change-import/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/no-change-import/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/no-change-import/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/no-change-import/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/no-change-import/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/no-change-import/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/no-change-import/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/no-change-import/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/no-change-require/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/no-change-require/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/no-change-require/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/no-change-require/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/no-change-require/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/no-change-require/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/no-change-require/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/no-change-require/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require-alias/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require-alias/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require-alias/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require-alias/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require-alias/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require-alias/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require-alias/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-direct/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-direct/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require-destructured-direct/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-direct/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-multi/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-multi/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require-destructured-multi/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-multi/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-only/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require-destructured-only/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-only/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require-destructured-only/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require-destructured-only/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/require/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/require/input.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/require/metrics.json b/codemods/jssg/react-proptypes-to-prop-types/tests/require/metrics.json similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/require/metrics.json rename to codemods/jssg/react-proptypes-to-prop-types/tests/require/metrics.json diff --git a/codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/with-top-comment/expected.tsx diff --git a/codemods/react-proptypes-to-prop-types/tests/with-top-comment/input.tsx b/codemods/jssg/react-proptypes-to-prop-types/tests/with-top-comment/input.tsx similarity index 100% rename from codemods/react-proptypes-to-prop-types/tests/with-top-comment/input.tsx rename to codemods/jssg/react-proptypes-to-prop-types/tests/with-top-comment/input.tsx diff --git a/codemods/create-element-to-jsx/tsconfig.json b/codemods/jssg/react-proptypes-to-prop-types/tsconfig.json similarity index 100% rename from codemods/create-element-to-jsx/tsconfig.json rename to codemods/jssg/react-proptypes-to-prop-types/tsconfig.json diff --git a/codemods/react-proptypes-to-prop-types/workflow.yaml b/codemods/jssg/react-proptypes-to-prop-types/workflow.yaml similarity index 100% rename from codemods/react-proptypes-to-prop-types/workflow.yaml rename to codemods/jssg/react-proptypes-to-prop-types/workflow.yaml diff --git a/codemods/replace-act-import/README.md b/codemods/jssg/replace-act-import/README.md similarity index 53% rename from codemods/replace-act-import/README.md rename to codemods/jssg/replace-act-import/README.md index 14085c2..96cfdb4 100644 --- a/codemods/replace-act-import/README.md +++ b/codemods/jssg/replace-act-import/README.md @@ -1,11 +1,13 @@ -# @react/replace-act-import +# replace-act-import Move `act` usage from `react-dom/test-utils` to `react` across named imports, namespace imports, and re-exports. -## Usage +## Run Locally + +Run this command from this directory: ```bash -codemod run @react/replace-act-import +pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts ``` ## Development diff --git a/codemods/replace-act-import/codemod.yaml b/codemods/jssg/replace-act-import/codemod.yaml similarity index 100% rename from codemods/replace-act-import/codemod.yaml rename to codemods/jssg/replace-act-import/codemod.yaml diff --git a/codemods/replace-act-import/package.json b/codemods/jssg/replace-act-import/package.json similarity index 100% rename from codemods/replace-act-import/package.json rename to codemods/jssg/replace-act-import/package.json diff --git a/codemods/replace-act-import/scripts/codemod.ts b/codemods/jssg/replace-act-import/scripts/codemod.ts similarity index 100% rename from codemods/replace-act-import/scripts/codemod.ts rename to codemods/jssg/replace-act-import/scripts/codemod.ts diff --git a/codemods/replace-act-import/tests/default-import/expected.tsx b/codemods/jssg/replace-act-import/tests/default-import/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/default-import/expected.tsx rename to codemods/jssg/replace-act-import/tests/default-import/expected.tsx diff --git a/codemods/replace-act-import/tests/default-import/input.tsx b/codemods/jssg/replace-act-import/tests/default-import/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/default-import/input.tsx rename to codemods/jssg/replace-act-import/tests/default-import/input.tsx diff --git a/codemods/replace-act-import/tests/default-import/metrics.json b/codemods/jssg/replace-act-import/tests/default-import/metrics.json similarity index 100% rename from codemods/replace-act-import/tests/default-import/metrics.json rename to codemods/jssg/replace-act-import/tests/default-import/metrics.json diff --git a/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx b/codemods/jssg/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx rename to codemods/jssg/replace-act-import/tests/multiple-named-imports-with-other-specifier/expected.tsx diff --git a/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/input.tsx b/codemods/jssg/replace-act-import/tests/multiple-named-imports-with-other-specifier/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/input.tsx rename to codemods/jssg/replace-act-import/tests/multiple-named-imports-with-other-specifier/input.tsx diff --git a/codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/metrics.json b/codemods/jssg/replace-act-import/tests/multiple-named-imports-with-other-specifier/metrics.json similarity index 100% rename from codemods/replace-act-import/tests/multiple-named-imports-with-other-specifier/metrics.json rename to codemods/jssg/replace-act-import/tests/multiple-named-imports-with-other-specifier/metrics.json diff --git a/codemods/replace-act-import/tests/multiple-named-imports/expected.tsx b/codemods/jssg/replace-act-import/tests/multiple-named-imports/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/multiple-named-imports/expected.tsx rename to codemods/jssg/replace-act-import/tests/multiple-named-imports/expected.tsx diff --git a/codemods/replace-act-import/tests/multiple-named-imports/input.tsx b/codemods/jssg/replace-act-import/tests/multiple-named-imports/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/multiple-named-imports/input.tsx rename to codemods/jssg/replace-act-import/tests/multiple-named-imports/input.tsx diff --git a/codemods/replace-act-import/tests/multiple-named-imports/metrics.json b/codemods/jssg/replace-act-import/tests/multiple-named-imports/metrics.json similarity index 100% rename from codemods/replace-act-import/tests/multiple-named-imports/metrics.json rename to codemods/jssg/replace-act-import/tests/multiple-named-imports/metrics.json diff --git a/codemods/replace-act-import/tests/named-import-2/expected.tsx b/codemods/jssg/replace-act-import/tests/named-import-2/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/named-import-2/expected.tsx rename to codemods/jssg/replace-act-import/tests/named-import-2/expected.tsx diff --git a/codemods/replace-act-import/tests/named-import-2/input.tsx b/codemods/jssg/replace-act-import/tests/named-import-2/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/named-import-2/input.tsx rename to codemods/jssg/replace-act-import/tests/named-import-2/input.tsx diff --git a/codemods/replace-act-import/tests/named-import-2/metrics.json b/codemods/jssg/replace-act-import/tests/named-import-2/metrics.json similarity index 100% rename from codemods/replace-act-import/tests/named-import-2/metrics.json rename to codemods/jssg/replace-act-import/tests/named-import-2/metrics.json diff --git a/codemods/replace-act-import/tests/named-import/expected.tsx b/codemods/jssg/replace-act-import/tests/named-import/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/named-import/expected.tsx rename to codemods/jssg/replace-act-import/tests/named-import/expected.tsx diff --git a/codemods/replace-act-import/tests/named-import/input.tsx b/codemods/jssg/replace-act-import/tests/named-import/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/named-import/input.tsx rename to codemods/jssg/replace-act-import/tests/named-import/input.tsx diff --git a/codemods/replace-act-import/tests/named-import/metrics.json b/codemods/jssg/replace-act-import/tests/named-import/metrics.json similarity index 100% rename from codemods/replace-act-import/tests/named-import/metrics.json rename to codemods/jssg/replace-act-import/tests/named-import/metrics.json diff --git a/codemods/replace-act-import/tests/named-reexport-no-change/expected.tsx b/codemods/jssg/replace-act-import/tests/named-reexport-no-change/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/named-reexport-no-change/expected.tsx rename to codemods/jssg/replace-act-import/tests/named-reexport-no-change/expected.tsx diff --git a/codemods/replace-act-import/tests/named-reexport-no-change/input.tsx b/codemods/jssg/replace-act-import/tests/named-reexport-no-change/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/named-reexport-no-change/input.tsx rename to codemods/jssg/replace-act-import/tests/named-reexport-no-change/input.tsx diff --git a/codemods/replace-act-import/tests/other-import/expected.tsx b/codemods/jssg/replace-act-import/tests/other-import/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/other-import/expected.tsx rename to codemods/jssg/replace-act-import/tests/other-import/expected.tsx diff --git a/codemods/replace-act-import/tests/other-import/input.tsx b/codemods/jssg/replace-act-import/tests/other-import/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/other-import/input.tsx rename to codemods/jssg/replace-act-import/tests/other-import/input.tsx diff --git a/codemods/replace-act-import/tests/re-export/expected.tsx b/codemods/jssg/replace-act-import/tests/re-export/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/re-export/expected.tsx rename to codemods/jssg/replace-act-import/tests/re-export/expected.tsx diff --git a/codemods/replace-act-import/tests/re-export/input.tsx b/codemods/jssg/replace-act-import/tests/re-export/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/re-export/input.tsx rename to codemods/jssg/replace-act-import/tests/re-export/input.tsx diff --git a/codemods/replace-act-import/tests/re-export/metrics.json b/codemods/jssg/replace-act-import/tests/re-export/metrics.json similarity index 100% rename from codemods/replace-act-import/tests/re-export/metrics.json rename to codemods/jssg/replace-act-import/tests/re-export/metrics.json diff --git a/codemods/replace-act-import/tests/wildcard-import-2/expected.tsx b/codemods/jssg/replace-act-import/tests/wildcard-import-2/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/wildcard-import-2/expected.tsx rename to codemods/jssg/replace-act-import/tests/wildcard-import-2/expected.tsx diff --git a/codemods/replace-act-import/tests/wildcard-import-2/input.tsx b/codemods/jssg/replace-act-import/tests/wildcard-import-2/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/wildcard-import-2/input.tsx rename to codemods/jssg/replace-act-import/tests/wildcard-import-2/input.tsx diff --git a/codemods/replace-act-import/tests/wildcard-import-2/metrics.json b/codemods/jssg/replace-act-import/tests/wildcard-import-2/metrics.json similarity index 100% rename from codemods/replace-act-import/tests/wildcard-import-2/metrics.json rename to codemods/jssg/replace-act-import/tests/wildcard-import-2/metrics.json diff --git a/codemods/replace-act-import/tests/wildcard-import/expected.tsx b/codemods/jssg/replace-act-import/tests/wildcard-import/expected.tsx similarity index 100% rename from codemods/replace-act-import/tests/wildcard-import/expected.tsx rename to codemods/jssg/replace-act-import/tests/wildcard-import/expected.tsx diff --git a/codemods/replace-act-import/tests/wildcard-import/input.tsx b/codemods/jssg/replace-act-import/tests/wildcard-import/input.tsx similarity index 100% rename from codemods/replace-act-import/tests/wildcard-import/input.tsx rename to codemods/jssg/replace-act-import/tests/wildcard-import/input.tsx diff --git a/codemods/replace-act-import/tests/wildcard-import/metrics.json b/codemods/jssg/replace-act-import/tests/wildcard-import/metrics.json similarity index 100% rename from codemods/replace-act-import/tests/wildcard-import/metrics.json rename to codemods/jssg/replace-act-import/tests/wildcard-import/metrics.json diff --git a/codemods/error-boundaries/tsconfig.json b/codemods/jssg/replace-act-import/tsconfig.json similarity index 100% rename from codemods/error-boundaries/tsconfig.json rename to codemods/jssg/replace-act-import/tsconfig.json diff --git a/codemods/replace-act-import/workflow.yaml b/codemods/jssg/replace-act-import/workflow.yaml similarity index 100% rename from codemods/replace-act-import/workflow.yaml rename to codemods/jssg/replace-act-import/workflow.yaml diff --git a/codemods/rename-unsafe-lifecycles/.gitignore b/codemods/jssg/replace-reactdom-render/.gitignore similarity index 100% rename from codemods/rename-unsafe-lifecycles/.gitignore rename to codemods/jssg/replace-reactdom-render/.gitignore diff --git a/codemods/jssg/replace-reactdom-render/README.md b/codemods/jssg/replace-reactdom-render/README.md new file mode 100644 index 0000000..4d8224c --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/README.md @@ -0,0 +1,18 @@ +# replace-reactdom-render + +Replace `ReactDOM.render` and named `render` imports with `createRoot(...).render(...)`. + +## Run Locally + +Run this command from this directory: + +```bash +pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/replace-reactdom-render/codemod.yaml b/codemods/jssg/replace-reactdom-render/codemod.yaml similarity index 100% rename from codemods/replace-reactdom-render/codemod.yaml rename to codemods/jssg/replace-reactdom-render/codemod.yaml diff --git a/codemods/replace-reactdom-render/package.json b/codemods/jssg/replace-reactdom-render/package.json similarity index 100% rename from codemods/replace-reactdom-render/package.json rename to codemods/jssg/replace-reactdom-render/package.json diff --git a/codemods/replace-reactdom-render/pnpm-lock.yaml b/codemods/jssg/replace-reactdom-render/pnpm-lock.yaml similarity index 100% rename from codemods/replace-reactdom-render/pnpm-lock.yaml rename to codemods/jssg/replace-reactdom-render/pnpm-lock.yaml diff --git a/codemods/replace-reactdom-render/scripts/codemod.ts b/codemods/jssg/replace-reactdom-render/scripts/codemod.ts similarity index 100% rename from codemods/replace-reactdom-render/scripts/codemod.ts rename to codemods/jssg/replace-reactdom-render/scripts/codemod.ts diff --git a/codemods/replace-reactdom-render/tests/default/expected.tsx b/codemods/jssg/replace-reactdom-render/tests/default/expected.tsx similarity index 100% rename from codemods/replace-reactdom-render/tests/default/expected.tsx rename to codemods/jssg/replace-reactdom-render/tests/default/expected.tsx diff --git a/codemods/replace-reactdom-render/tests/default/input.tsx b/codemods/jssg/replace-reactdom-render/tests/default/input.tsx similarity index 100% rename from codemods/replace-reactdom-render/tests/default/input.tsx rename to codemods/jssg/replace-reactdom-render/tests/default/input.tsx diff --git a/codemods/replace-reactdom-render/tests/default/metrics.json b/codemods/jssg/replace-reactdom-render/tests/default/metrics.json similarity index 100% rename from codemods/replace-reactdom-render/tests/default/metrics.json rename to codemods/jssg/replace-reactdom-render/tests/default/metrics.json diff --git a/codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx b/codemods/jssg/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx similarity index 100% rename from codemods/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx rename to codemods/jssg/replace-reactdom-render/tests/multiple-member-render-imports/expected.tsx diff --git a/codemods/replace-reactdom-render/tests/multiple-member-render-imports/input.tsx b/codemods/jssg/replace-reactdom-render/tests/multiple-member-render-imports/input.tsx similarity index 100% rename from codemods/replace-reactdom-render/tests/multiple-member-render-imports/input.tsx rename to codemods/jssg/replace-reactdom-render/tests/multiple-member-render-imports/input.tsx diff --git a/codemods/replace-reactdom-render/tests/multiple-member-render-imports/metrics.json b/codemods/jssg/replace-reactdom-render/tests/multiple-member-render-imports/metrics.json similarity index 100% rename from codemods/replace-reactdom-render/tests/multiple-member-render-imports/metrics.json rename to codemods/jssg/replace-reactdom-render/tests/multiple-member-render-imports/metrics.json diff --git a/codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx b/codemods/jssg/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx similarity index 100% rename from codemods/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx rename to codemods/jssg/replace-reactdom-render/tests/multiple-named-render-imports/expected.tsx diff --git a/codemods/replace-reactdom-render/tests/multiple-named-render-imports/input.tsx b/codemods/jssg/replace-reactdom-render/tests/multiple-named-render-imports/input.tsx similarity index 100% rename from codemods/replace-reactdom-render/tests/multiple-named-render-imports/input.tsx rename to codemods/jssg/replace-reactdom-render/tests/multiple-named-render-imports/input.tsx diff --git a/codemods/replace-reactdom-render/tests/multiple-named-render-imports/metrics.json b/codemods/jssg/replace-reactdom-render/tests/multiple-named-render-imports/metrics.json similarity index 100% rename from codemods/replace-reactdom-render/tests/multiple-named-render-imports/metrics.json rename to codemods/jssg/replace-reactdom-render/tests/multiple-named-render-imports/metrics.json diff --git a/codemods/replace-reactdom-render/tests/nested/expected.tsx b/codemods/jssg/replace-reactdom-render/tests/nested/expected.tsx similarity index 100% rename from codemods/replace-reactdom-render/tests/nested/expected.tsx rename to codemods/jssg/replace-reactdom-render/tests/nested/expected.tsx diff --git a/codemods/replace-reactdom-render/tests/nested/input.tsx b/codemods/jssg/replace-reactdom-render/tests/nested/input.tsx similarity index 100% rename from codemods/replace-reactdom-render/tests/nested/input.tsx rename to codemods/jssg/replace-reactdom-render/tests/nested/input.tsx diff --git a/codemods/replace-reactdom-render/tests/nested/metrics.json b/codemods/jssg/replace-reactdom-render/tests/nested/metrics.json similarity index 100% rename from codemods/replace-reactdom-render/tests/nested/metrics.json rename to codemods/jssg/replace-reactdom-render/tests/nested/metrics.json diff --git a/codemods/find-dom-node/tsconfig.json b/codemods/jssg/replace-reactdom-render/tsconfig.json similarity index 100% rename from codemods/find-dom-node/tsconfig.json rename to codemods/jssg/replace-reactdom-render/tsconfig.json diff --git a/codemods/create-element-to-jsx/workflow.yaml b/codemods/jssg/replace-reactdom-render/workflow.yaml similarity index 100% rename from codemods/create-element-to-jsx/workflow.yaml rename to codemods/jssg/replace-reactdom-render/workflow.yaml diff --git a/codemods/replace-reactdom-render/.gitignore b/codemods/jssg/replace-string-ref/.gitignore similarity index 100% rename from codemods/replace-reactdom-render/.gitignore rename to codemods/jssg/replace-string-ref/.gitignore diff --git a/codemods/replace-string-ref/README.md b/codemods/jssg/replace-string-ref/README.md similarity index 51% rename from codemods/replace-string-ref/README.md rename to codemods/jssg/replace-string-ref/README.md index a371e19..37ed12d 100644 --- a/codemods/replace-string-ref/README.md +++ b/codemods/jssg/replace-string-ref/README.md @@ -1,11 +1,13 @@ -# @react/replace-string-ref +# replace-string-ref Replace string refs in React class components with callback refs that assign through `this.refs`. -## Usage +## Run Locally + +Run this command from this directory: ```bash -codemod run @react/replace-string-ref +pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts ``` ## Development diff --git a/codemods/replace-string-ref/codemod.yaml b/codemods/jssg/replace-string-ref/codemod.yaml similarity index 100% rename from codemods/replace-string-ref/codemod.yaml rename to codemods/jssg/replace-string-ref/codemod.yaml diff --git a/codemods/replace-string-ref/package.json b/codemods/jssg/replace-string-ref/package.json similarity index 100% rename from codemods/replace-string-ref/package.json rename to codemods/jssg/replace-string-ref/package.json diff --git a/codemods/replace-string-ref/pnpm-lock.yaml b/codemods/jssg/replace-string-ref/pnpm-lock.yaml similarity index 100% rename from codemods/replace-string-ref/pnpm-lock.yaml rename to codemods/jssg/replace-string-ref/pnpm-lock.yaml diff --git a/codemods/replace-string-ref/scripts/codemod.ts b/codemods/jssg/replace-string-ref/scripts/codemod.ts similarity index 100% rename from codemods/replace-string-ref/scripts/codemod.ts rename to codemods/jssg/replace-string-ref/scripts/codemod.ts diff --git a/codemods/replace-string-ref/tests/class-component-custom-import-names/expected.tsx b/codemods/jssg/replace-string-ref/tests/class-component-custom-import-names/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/class-component-custom-import-names/expected.tsx rename to codemods/jssg/replace-string-ref/tests/class-component-custom-import-names/expected.tsx diff --git a/codemods/replace-string-ref/tests/class-component-custom-import-names/input.tsx b/codemods/jssg/replace-string-ref/tests/class-component-custom-import-names/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/class-component-custom-import-names/input.tsx rename to codemods/jssg/replace-string-ref/tests/class-component-custom-import-names/input.tsx diff --git a/codemods/replace-string-ref/tests/class-component-custom-import-names/metrics.json b/codemods/jssg/replace-string-ref/tests/class-component-custom-import-names/metrics.json similarity index 100% rename from codemods/replace-string-ref/tests/class-component-custom-import-names/metrics.json rename to codemods/jssg/replace-string-ref/tests/class-component-custom-import-names/metrics.json diff --git a/codemods/replace-string-ref/tests/class-component-default-import/expected.tsx b/codemods/jssg/replace-string-ref/tests/class-component-default-import/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/class-component-default-import/expected.tsx rename to codemods/jssg/replace-string-ref/tests/class-component-default-import/expected.tsx diff --git a/codemods/replace-string-ref/tests/class-component-default-import/input.tsx b/codemods/jssg/replace-string-ref/tests/class-component-default-import/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/class-component-default-import/input.tsx rename to codemods/jssg/replace-string-ref/tests/class-component-default-import/input.tsx diff --git a/codemods/replace-string-ref/tests/class-component-default-import/metrics.json b/codemods/jssg/replace-string-ref/tests/class-component-default-import/metrics.json similarity index 100% rename from codemods/replace-string-ref/tests/class-component-default-import/metrics.json rename to codemods/jssg/replace-string-ref/tests/class-component-default-import/metrics.json diff --git a/codemods/replace-string-ref/tests/class-component-named-import/expected.tsx b/codemods/jssg/replace-string-ref/tests/class-component-named-import/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/class-component-named-import/expected.tsx rename to codemods/jssg/replace-string-ref/tests/class-component-named-import/expected.tsx diff --git a/codemods/replace-string-ref/tests/class-component-named-import/input.tsx b/codemods/jssg/replace-string-ref/tests/class-component-named-import/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/class-component-named-import/input.tsx rename to codemods/jssg/replace-string-ref/tests/class-component-named-import/input.tsx diff --git a/codemods/replace-string-ref/tests/class-component-named-import/metrics.json b/codemods/jssg/replace-string-ref/tests/class-component-named-import/metrics.json similarity index 100% rename from codemods/replace-string-ref/tests/class-component-named-import/metrics.json rename to codemods/jssg/replace-string-ref/tests/class-component-named-import/metrics.json diff --git a/codemods/replace-string-ref/tests/export-default-class/expected.tsx b/codemods/jssg/replace-string-ref/tests/export-default-class/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/export-default-class/expected.tsx rename to codemods/jssg/replace-string-ref/tests/export-default-class/expected.tsx diff --git a/codemods/replace-string-ref/tests/export-default-class/input.tsx b/codemods/jssg/replace-string-ref/tests/export-default-class/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/export-default-class/input.tsx rename to codemods/jssg/replace-string-ref/tests/export-default-class/input.tsx diff --git a/codemods/replace-string-ref/tests/export-default-class/metrics.json b/codemods/jssg/replace-string-ref/tests/export-default-class/metrics.json similarity index 100% rename from codemods/replace-string-ref/tests/export-default-class/metrics.json rename to codemods/jssg/replace-string-ref/tests/export-default-class/metrics.json diff --git a/codemods/replace-string-ref/tests/function-component/expected.tsx b/codemods/jssg/replace-string-ref/tests/function-component/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/function-component/expected.tsx rename to codemods/jssg/replace-string-ref/tests/function-component/expected.tsx diff --git a/codemods/replace-string-ref/tests/function-component/input.tsx b/codemods/jssg/replace-string-ref/tests/function-component/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/function-component/input.tsx rename to codemods/jssg/replace-string-ref/tests/function-component/input.tsx diff --git a/codemods/replace-string-ref/tests/member-expression-ref-value-no-change/expected.tsx b/codemods/jssg/replace-string-ref/tests/member-expression-ref-value-no-change/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/member-expression-ref-value-no-change/expected.tsx rename to codemods/jssg/replace-string-ref/tests/member-expression-ref-value-no-change/expected.tsx diff --git a/codemods/replace-string-ref/tests/member-expression-ref-value-no-change/input.tsx b/codemods/jssg/replace-string-ref/tests/member-expression-ref-value-no-change/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/member-expression-ref-value-no-change/input.tsx rename to codemods/jssg/replace-string-ref/tests/member-expression-ref-value-no-change/input.tsx diff --git a/codemods/replace-string-ref/tests/mixin-component-no-change/expected.tsx b/codemods/jssg/replace-string-ref/tests/mixin-component-no-change/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/mixin-component-no-change/expected.tsx rename to codemods/jssg/replace-string-ref/tests/mixin-component-no-change/expected.tsx diff --git a/codemods/replace-string-ref/tests/mixin-component-no-change/input.tsx b/codemods/jssg/replace-string-ref/tests/mixin-component-no-change/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/mixin-component-no-change/input.tsx rename to codemods/jssg/replace-string-ref/tests/mixin-component-no-change/input.tsx diff --git a/codemods/replace-string-ref/tests/multiple-string-refs/expected.tsx b/codemods/jssg/replace-string-ref/tests/multiple-string-refs/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/multiple-string-refs/expected.tsx rename to codemods/jssg/replace-string-ref/tests/multiple-string-refs/expected.tsx diff --git a/codemods/replace-string-ref/tests/multiple-string-refs/input.tsx b/codemods/jssg/replace-string-ref/tests/multiple-string-refs/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/multiple-string-refs/input.tsx rename to codemods/jssg/replace-string-ref/tests/multiple-string-refs/input.tsx diff --git a/codemods/replace-string-ref/tests/multiple-string-refs/metrics.json b/codemods/jssg/replace-string-ref/tests/multiple-string-refs/metrics.json similarity index 100% rename from codemods/replace-string-ref/tests/multiple-string-refs/metrics.json rename to codemods/jssg/replace-string-ref/tests/multiple-string-refs/metrics.json diff --git a/codemods/replace-string-ref/tests/namespace-import/expected.tsx b/codemods/jssg/replace-string-ref/tests/namespace-import/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/namespace-import/expected.tsx rename to codemods/jssg/replace-string-ref/tests/namespace-import/expected.tsx diff --git a/codemods/replace-string-ref/tests/namespace-import/input.tsx b/codemods/jssg/replace-string-ref/tests/namespace-import/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/namespace-import/input.tsx rename to codemods/jssg/replace-string-ref/tests/namespace-import/input.tsx diff --git a/codemods/replace-string-ref/tests/namespace-import/metrics.json b/codemods/jssg/replace-string-ref/tests/namespace-import/metrics.json similarity index 100% rename from codemods/replace-string-ref/tests/namespace-import/metrics.json rename to codemods/jssg/replace-string-ref/tests/namespace-import/metrics.json diff --git a/codemods/replace-string-ref/tests/non-identifier-ref-name/expected.tsx b/codemods/jssg/replace-string-ref/tests/non-identifier-ref-name/expected.tsx similarity index 100% rename from codemods/replace-string-ref/tests/non-identifier-ref-name/expected.tsx rename to codemods/jssg/replace-string-ref/tests/non-identifier-ref-name/expected.tsx diff --git a/codemods/replace-string-ref/tests/non-identifier-ref-name/input.tsx b/codemods/jssg/replace-string-ref/tests/non-identifier-ref-name/input.tsx similarity index 100% rename from codemods/replace-string-ref/tests/non-identifier-ref-name/input.tsx rename to codemods/jssg/replace-string-ref/tests/non-identifier-ref-name/input.tsx diff --git a/codemods/replace-string-ref/tests/non-identifier-ref-name/metrics.json b/codemods/jssg/replace-string-ref/tests/non-identifier-ref-name/metrics.json similarity index 100% rename from codemods/replace-string-ref/tests/non-identifier-ref-name/metrics.json rename to codemods/jssg/replace-string-ref/tests/non-identifier-ref-name/metrics.json diff --git a/codemods/pure-component/tsconfig.json b/codemods/jssg/replace-string-ref/tsconfig.json similarity index 100% rename from codemods/pure-component/tsconfig.json rename to codemods/jssg/replace-string-ref/tsconfig.json diff --git a/codemods/error-boundaries/workflow.yaml b/codemods/jssg/replace-string-ref/workflow.yaml similarity index 100% rename from codemods/error-boundaries/workflow.yaml rename to codemods/jssg/replace-string-ref/workflow.yaml diff --git a/codemods/jssg/replace-use-form-state/README.md b/codemods/jssg/replace-use-form-state/README.md new file mode 100644 index 0000000..b6fff1f --- /dev/null +++ b/codemods/jssg/replace-use-form-state/README.md @@ -0,0 +1,18 @@ +# replace-use-form-state + +Rename `useFormState` to `useActionState` in `react-dom` imports and usages. + +## Run Locally + +Run this command from this directory: + +```bash +pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/replace-use-form-state/codemod.yaml b/codemods/jssg/replace-use-form-state/codemod.yaml similarity index 100% rename from codemods/replace-use-form-state/codemod.yaml rename to codemods/jssg/replace-use-form-state/codemod.yaml diff --git a/codemods/replace-use-form-state/package.json b/codemods/jssg/replace-use-form-state/package.json similarity index 100% rename from codemods/replace-use-form-state/package.json rename to codemods/jssg/replace-use-form-state/package.json diff --git a/codemods/replace-use-form-state/scripts/codemod.ts b/codemods/jssg/replace-use-form-state/scripts/codemod.ts similarity index 100% rename from codemods/replace-use-form-state/scripts/codemod.ts rename to codemods/jssg/replace-use-form-state/scripts/codemod.ts diff --git a/codemods/replace-use-form-state/tests/aliased-to-same-name/expected.tsx b/codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/aliased-to-same-name/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/expected.tsx diff --git a/codemods/replace-use-form-state/tests/aliased-to-same-name/input.tsx b/codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/aliased-to-same-name/input.tsx rename to codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/input.tsx diff --git a/codemods/replace-use-form-state/tests/aliased-to-same-name/metrics.json b/codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/aliased-to-same-name/metrics.json rename to codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/metrics.json diff --git a/codemods/replace-use-form-state/tests/default-and-named/expected.tsx b/codemods/jssg/replace-use-form-state/tests/default-and-named/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/default-and-named/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/default-and-named/expected.tsx diff --git a/codemods/replace-use-form-state/tests/default-and-named/input.tsx b/codemods/jssg/replace-use-form-state/tests/default-and-named/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/default-and-named/input.tsx rename to codemods/jssg/replace-use-form-state/tests/default-and-named/input.tsx diff --git a/codemods/replace-use-form-state/tests/default-and-named/metrics.json b/codemods/jssg/replace-use-form-state/tests/default-and-named/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/default-and-named/metrics.json rename to codemods/jssg/replace-use-form-state/tests/default-and-named/metrics.json diff --git a/codemods/replace-use-form-state/tests/default-import/expected.tsx b/codemods/jssg/replace-use-form-state/tests/default-import/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/default-import/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/default-import/expected.tsx diff --git a/codemods/replace-use-form-state/tests/default-import/input.tsx b/codemods/jssg/replace-use-form-state/tests/default-import/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/default-import/input.tsx rename to codemods/jssg/replace-use-form-state/tests/default-import/input.tsx diff --git a/codemods/replace-use-form-state/tests/default-import/metrics.json b/codemods/jssg/replace-use-form-state/tests/default-import/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/default-import/metrics.json rename to codemods/jssg/replace-use-form-state/tests/default-import/metrics.json diff --git a/codemods/replace-use-form-state/tests/member-access-non-import-no-change/expected.tsx b/codemods/jssg/replace-use-form-state/tests/member-access-non-import-no-change/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/member-access-non-import-no-change/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/member-access-non-import-no-change/expected.tsx diff --git a/codemods/replace-use-form-state/tests/member-access-non-import-no-change/input.tsx b/codemods/jssg/replace-use-form-state/tests/member-access-non-import-no-change/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/member-access-non-import-no-change/input.tsx rename to codemods/jssg/replace-use-form-state/tests/member-access-non-import-no-change/input.tsx diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/input.tsx b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/input.tsx rename to codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/input.tsx diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/metrics.json b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/metrics.json rename to codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/metrics.json diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/input.tsx b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/input.tsx rename to codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/input.tsx diff --git a/codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/metrics.json b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/metrics.json rename to codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/metrics.json diff --git a/codemods/replace-use-form-state/tests/named-import-2/expected.tsx b/codemods/jssg/replace-use-form-state/tests/named-import-2/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/named-import-2/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/named-import-2/expected.tsx diff --git a/codemods/replace-use-form-state/tests/named-import-2/input.tsx b/codemods/jssg/replace-use-form-state/tests/named-import-2/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/named-import-2/input.tsx rename to codemods/jssg/replace-use-form-state/tests/named-import-2/input.tsx diff --git a/codemods/replace-use-form-state/tests/named-import-2/metrics.json b/codemods/jssg/replace-use-form-state/tests/named-import-2/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/named-import-2/metrics.json rename to codemods/jssg/replace-use-form-state/tests/named-import-2/metrics.json diff --git a/codemods/replace-use-form-state/tests/named-import-3/expected.tsx b/codemods/jssg/replace-use-form-state/tests/named-import-3/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/named-import-3/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/named-import-3/expected.tsx diff --git a/codemods/replace-use-form-state/tests/named-import-3/input.tsx b/codemods/jssg/replace-use-form-state/tests/named-import-3/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/named-import-3/input.tsx rename to codemods/jssg/replace-use-form-state/tests/named-import-3/input.tsx diff --git a/codemods/replace-use-form-state/tests/named-import-3/metrics.json b/codemods/jssg/replace-use-form-state/tests/named-import-3/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/named-import-3/metrics.json rename to codemods/jssg/replace-use-form-state/tests/named-import-3/metrics.json diff --git a/codemods/replace-use-form-state/tests/named-import/expected.tsx b/codemods/jssg/replace-use-form-state/tests/named-import/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/named-import/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/named-import/expected.tsx diff --git a/codemods/replace-use-form-state/tests/named-import/input.tsx b/codemods/jssg/replace-use-form-state/tests/named-import/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/named-import/input.tsx rename to codemods/jssg/replace-use-form-state/tests/named-import/input.tsx diff --git a/codemods/replace-use-form-state/tests/named-import/metrics.json b/codemods/jssg/replace-use-form-state/tests/named-import/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/named-import/metrics.json rename to codemods/jssg/replace-use-form-state/tests/named-import/metrics.json diff --git a/codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx b/codemods/jssg/replace-use-form-state/tests/namespace-and-named/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/namespace-and-named/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/namespace-and-named/expected.tsx diff --git a/codemods/replace-use-form-state/tests/namespace-and-named/input.tsx b/codemods/jssg/replace-use-form-state/tests/namespace-and-named/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/namespace-and-named/input.tsx rename to codemods/jssg/replace-use-form-state/tests/namespace-and-named/input.tsx diff --git a/codemods/replace-use-form-state/tests/namespace-and-named/metrics.json b/codemods/jssg/replace-use-form-state/tests/namespace-and-named/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/namespace-and-named/metrics.json rename to codemods/jssg/replace-use-form-state/tests/namespace-and-named/metrics.json diff --git a/codemods/replace-use-form-state/tests/other-import/expected.tsx b/codemods/jssg/replace-use-form-state/tests/other-import/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/other-import/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/other-import/expected.tsx diff --git a/codemods/replace-use-form-state/tests/other-import/input.tsx b/codemods/jssg/replace-use-form-state/tests/other-import/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/other-import/input.tsx rename to codemods/jssg/replace-use-form-state/tests/other-import/input.tsx diff --git a/codemods/replace-use-form-state/tests/shadowed-local-binding/expected.tsx b/codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/shadowed-local-binding/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/expected.tsx diff --git a/codemods/replace-use-form-state/tests/shadowed-local-binding/input.tsx b/codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/shadowed-local-binding/input.tsx rename to codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/input.tsx diff --git a/codemods/replace-use-form-state/tests/shadowed-local-binding/metrics.json b/codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/shadowed-local-binding/metrics.json rename to codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/metrics.json diff --git a/codemods/replace-use-form-state/tests/type-identifier-rename/expected.tsx b/codemods/jssg/replace-use-form-state/tests/type-identifier-rename/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/type-identifier-rename/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/type-identifier-rename/expected.tsx diff --git a/codemods/replace-use-form-state/tests/type-identifier-rename/input.tsx b/codemods/jssg/replace-use-form-state/tests/type-identifier-rename/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/type-identifier-rename/input.tsx rename to codemods/jssg/replace-use-form-state/tests/type-identifier-rename/input.tsx diff --git a/codemods/replace-use-form-state/tests/type-identifier-rename/metrics.json b/codemods/jssg/replace-use-form-state/tests/type-identifier-rename/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/type-identifier-rename/metrics.json rename to codemods/jssg/replace-use-form-state/tests/type-identifier-rename/metrics.json diff --git a/codemods/replace-use-form-state/tests/wildcard-import/expected.tsx b/codemods/jssg/replace-use-form-state/tests/wildcard-import/expected.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/wildcard-import/expected.tsx rename to codemods/jssg/replace-use-form-state/tests/wildcard-import/expected.tsx diff --git a/codemods/replace-use-form-state/tests/wildcard-import/input.tsx b/codemods/jssg/replace-use-form-state/tests/wildcard-import/input.tsx similarity index 100% rename from codemods/replace-use-form-state/tests/wildcard-import/input.tsx rename to codemods/jssg/replace-use-form-state/tests/wildcard-import/input.tsx diff --git a/codemods/replace-use-form-state/tests/wildcard-import/metrics.json b/codemods/jssg/replace-use-form-state/tests/wildcard-import/metrics.json similarity index 100% rename from codemods/replace-use-form-state/tests/wildcard-import/metrics.json rename to codemods/jssg/replace-use-form-state/tests/wildcard-import/metrics.json diff --git a/codemods/pure-render-mixin/tsconfig.json b/codemods/jssg/replace-use-form-state/tsconfig.json similarity index 100% rename from codemods/pure-render-mixin/tsconfig.json rename to codemods/jssg/replace-use-form-state/tsconfig.json diff --git a/codemods/replace-use-form-state/workflow.yaml b/codemods/jssg/replace-use-form-state/workflow.yaml similarity index 100% rename from codemods/replace-use-form-state/workflow.yaml rename to codemods/jssg/replace-use-form-state/workflow.yaml diff --git a/codemods/replace-string-ref/.gitignore b/codemods/jssg/use-context-hook/.gitignore similarity index 100% rename from codemods/replace-string-ref/.gitignore rename to codemods/jssg/use-context-hook/.gitignore diff --git a/codemods/jssg/use-context-hook/README.md b/codemods/jssg/use-context-hook/README.md new file mode 100644 index 0000000..7a7ed78 --- /dev/null +++ b/codemods/jssg/use-context-hook/README.md @@ -0,0 +1,18 @@ +# use-context-hook + +Transform `React.useContext()` and `useContext()` calls into `use()`. + +## Run Locally + +Run this command from this directory: + +```bash +pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +``` + +## Development + +```bash +pnpm test +pnpm check-types +``` diff --git a/codemods/use-context-hook/codemod.yaml b/codemods/jssg/use-context-hook/codemod.yaml similarity index 100% rename from codemods/use-context-hook/codemod.yaml rename to codemods/jssg/use-context-hook/codemod.yaml diff --git a/codemods/use-context-hook/package.json b/codemods/jssg/use-context-hook/package.json similarity index 100% rename from codemods/use-context-hook/package.json rename to codemods/jssg/use-context-hook/package.json diff --git a/codemods/update-react-imports/pnpm-lock.yaml b/codemods/jssg/use-context-hook/pnpm-lock.yaml similarity index 100% rename from codemods/update-react-imports/pnpm-lock.yaml rename to codemods/jssg/use-context-hook/pnpm-lock.yaml diff --git a/codemods/use-context-hook/scripts/codemod.ts b/codemods/jssg/use-context-hook/scripts/codemod.ts similarity index 100% rename from codemods/use-context-hook/scripts/codemod.ts rename to codemods/jssg/use-context-hook/scripts/codemod.ts diff --git a/codemods/use-context-hook/tests/aliased-use-context/expected.tsx b/codemods/jssg/use-context-hook/tests/aliased-use-context/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/aliased-use-context/expected.tsx rename to codemods/jssg/use-context-hook/tests/aliased-use-context/expected.tsx diff --git a/codemods/use-context-hook/tests/aliased-use-context/input.tsx b/codemods/jssg/use-context-hook/tests/aliased-use-context/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/aliased-use-context/input.tsx rename to codemods/jssg/use-context-hook/tests/aliased-use-context/input.tsx diff --git a/codemods/use-context-hook/tests/aliased-use-context/metrics.json b/codemods/jssg/use-context-hook/tests/aliased-use-context/metrics.json similarity index 100% rename from codemods/use-context-hook/tests/aliased-use-context/metrics.json rename to codemods/jssg/use-context-hook/tests/aliased-use-context/metrics.json diff --git a/codemods/use-context-hook/tests/any-use-context/expected.tsx b/codemods/jssg/use-context-hook/tests/any-use-context/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/any-use-context/expected.tsx rename to codemods/jssg/use-context-hook/tests/any-use-context/expected.tsx diff --git a/codemods/use-context-hook/tests/any-use-context/input.tsx b/codemods/jssg/use-context-hook/tests/any-use-context/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/any-use-context/input.tsx rename to codemods/jssg/use-context-hook/tests/any-use-context/input.tsx diff --git a/codemods/use-context-hook/tests/mixed-import/expected.tsx b/codemods/jssg/use-context-hook/tests/mixed-import/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/mixed-import/expected.tsx rename to codemods/jssg/use-context-hook/tests/mixed-import/expected.tsx diff --git a/codemods/use-context-hook/tests/mixed-import/input.tsx b/codemods/jssg/use-context-hook/tests/mixed-import/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/mixed-import/input.tsx rename to codemods/jssg/use-context-hook/tests/mixed-import/input.tsx diff --git a/codemods/use-context-hook/tests/mixed-import/metrics.json b/codemods/jssg/use-context-hook/tests/mixed-import/metrics.json similarity index 100% rename from codemods/use-context-hook/tests/mixed-import/metrics.json rename to codemods/jssg/use-context-hook/tests/mixed-import/metrics.json diff --git a/codemods/use-context-hook/tests/multiple-use-context-imports/expected.tsx b/codemods/jssg/use-context-hook/tests/multiple-use-context-imports/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/multiple-use-context-imports/expected.tsx rename to codemods/jssg/use-context-hook/tests/multiple-use-context-imports/expected.tsx diff --git a/codemods/use-context-hook/tests/multiple-use-context-imports/input.tsx b/codemods/jssg/use-context-hook/tests/multiple-use-context-imports/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/multiple-use-context-imports/input.tsx rename to codemods/jssg/use-context-hook/tests/multiple-use-context-imports/input.tsx diff --git a/codemods/use-context-hook/tests/multiple-use-context-imports/metrics.json b/codemods/jssg/use-context-hook/tests/multiple-use-context-imports/metrics.json similarity index 100% rename from codemods/use-context-hook/tests/multiple-use-context-imports/metrics.json rename to codemods/jssg/use-context-hook/tests/multiple-use-context-imports/metrics.json diff --git a/codemods/use-context-hook/tests/shadowed-local-binding/expected.tsx b/codemods/jssg/use-context-hook/tests/shadowed-local-binding/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/shadowed-local-binding/expected.tsx rename to codemods/jssg/use-context-hook/tests/shadowed-local-binding/expected.tsx diff --git a/codemods/use-context-hook/tests/shadowed-local-binding/input.tsx b/codemods/jssg/use-context-hook/tests/shadowed-local-binding/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/shadowed-local-binding/input.tsx rename to codemods/jssg/use-context-hook/tests/shadowed-local-binding/input.tsx diff --git a/codemods/use-context-hook/tests/shadowed-local-binding/metrics.json b/codemods/jssg/use-context-hook/tests/shadowed-local-binding/metrics.json similarity index 100% rename from codemods/use-context-hook/tests/shadowed-local-binding/metrics.json rename to codemods/jssg/use-context-hook/tests/shadowed-local-binding/metrics.json diff --git a/codemods/use-context-hook/tests/type-identifier-rename/expected.tsx b/codemods/jssg/use-context-hook/tests/type-identifier-rename/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/type-identifier-rename/expected.tsx rename to codemods/jssg/use-context-hook/tests/type-identifier-rename/expected.tsx diff --git a/codemods/use-context-hook/tests/type-identifier-rename/input.tsx b/codemods/jssg/use-context-hook/tests/type-identifier-rename/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/type-identifier-rename/input.tsx rename to codemods/jssg/use-context-hook/tests/type-identifier-rename/input.tsx diff --git a/codemods/use-context-hook/tests/type-identifier-rename/metrics.json b/codemods/jssg/use-context-hook/tests/type-identifier-rename/metrics.json similarity index 100% rename from codemods/use-context-hook/tests/type-identifier-rename/metrics.json rename to codemods/jssg/use-context-hook/tests/type-identifier-rename/metrics.json diff --git a/codemods/use-context-hook/tests/typescript-any-use-context/expected.tsx b/codemods/jssg/use-context-hook/tests/typescript-any-use-context/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/typescript-any-use-context/expected.tsx rename to codemods/jssg/use-context-hook/tests/typescript-any-use-context/expected.tsx diff --git a/codemods/use-context-hook/tests/typescript-any-use-context/input.tsx b/codemods/jssg/use-context-hook/tests/typescript-any-use-context/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/typescript-any-use-context/input.tsx rename to codemods/jssg/use-context-hook/tests/typescript-any-use-context/input.tsx diff --git a/codemods/use-context-hook/tests/typescript-use-context-2/expected.tsx b/codemods/jssg/use-context-hook/tests/typescript-use-context-2/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/typescript-use-context-2/expected.tsx rename to codemods/jssg/use-context-hook/tests/typescript-use-context-2/expected.tsx diff --git a/codemods/use-context-hook/tests/typescript-use-context-2/input.tsx b/codemods/jssg/use-context-hook/tests/typescript-use-context-2/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/typescript-use-context-2/input.tsx rename to codemods/jssg/use-context-hook/tests/typescript-use-context-2/input.tsx diff --git a/codemods/use-context-hook/tests/typescript-use-context-2/metrics.json b/codemods/jssg/use-context-hook/tests/typescript-use-context-2/metrics.json similarity index 100% rename from codemods/use-context-hook/tests/typescript-use-context-2/metrics.json rename to codemods/jssg/use-context-hook/tests/typescript-use-context-2/metrics.json diff --git a/codemods/use-context-hook/tests/typescript-use-context/expected.tsx b/codemods/jssg/use-context-hook/tests/typescript-use-context/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/typescript-use-context/expected.tsx rename to codemods/jssg/use-context-hook/tests/typescript-use-context/expected.tsx diff --git a/codemods/use-context-hook/tests/typescript-use-context/input.tsx b/codemods/jssg/use-context-hook/tests/typescript-use-context/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/typescript-use-context/input.tsx rename to codemods/jssg/use-context-hook/tests/typescript-use-context/input.tsx diff --git a/codemods/use-context-hook/tests/typescript-use-context/metrics.json b/codemods/jssg/use-context-hook/tests/typescript-use-context/metrics.json similarity index 100% rename from codemods/use-context-hook/tests/typescript-use-context/metrics.json rename to codemods/jssg/use-context-hook/tests/typescript-use-context/metrics.json diff --git a/codemods/use-context-hook/tests/use-context-2/expected.tsx b/codemods/jssg/use-context-hook/tests/use-context-2/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/use-context-2/expected.tsx rename to codemods/jssg/use-context-hook/tests/use-context-2/expected.tsx diff --git a/codemods/use-context-hook/tests/use-context-2/input.tsx b/codemods/jssg/use-context-hook/tests/use-context-2/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/use-context-2/input.tsx rename to codemods/jssg/use-context-hook/tests/use-context-2/input.tsx diff --git a/codemods/use-context-hook/tests/use-context-2/metrics.json b/codemods/jssg/use-context-hook/tests/use-context-2/metrics.json similarity index 100% rename from codemods/use-context-hook/tests/use-context-2/metrics.json rename to codemods/jssg/use-context-hook/tests/use-context-2/metrics.json diff --git a/codemods/use-context-hook/tests/use-context/expected.tsx b/codemods/jssg/use-context-hook/tests/use-context/expected.tsx similarity index 100% rename from codemods/use-context-hook/tests/use-context/expected.tsx rename to codemods/jssg/use-context-hook/tests/use-context/expected.tsx diff --git a/codemods/use-context-hook/tests/use-context/input.tsx b/codemods/jssg/use-context-hook/tests/use-context/input.tsx similarity index 100% rename from codemods/use-context-hook/tests/use-context/input.tsx rename to codemods/jssg/use-context-hook/tests/use-context/input.tsx diff --git a/codemods/use-context-hook/tests/use-context/metrics.json b/codemods/jssg/use-context-hook/tests/use-context/metrics.json similarity index 100% rename from codemods/use-context-hook/tests/use-context/metrics.json rename to codemods/jssg/use-context-hook/tests/use-context/metrics.json diff --git a/codemods/react-dom-to-react-dom-factories/tsconfig.json b/codemods/jssg/use-context-hook/tsconfig.json similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tsconfig.json rename to codemods/jssg/use-context-hook/tsconfig.json diff --git a/codemods/find-dom-node/workflow.yaml b/codemods/jssg/use-context-hook/workflow.yaml similarity index 100% rename from codemods/find-dom-node/workflow.yaml rename to codemods/jssg/use-context-hook/workflow.yaml diff --git a/codemods/legacy/.babelrc b/codemods/legacy/.babelrc new file mode 100644 index 0000000..5295ecd --- /dev/null +++ b/codemods/legacy/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/preset-env"], + "plugins": ["@babel/plugin-proposal-object-rest-spread"] +} diff --git a/codemods/legacy/jest/require-actual.d.ts b/codemods/legacy/jest/require-actual.d.ts new file mode 100644 index 0000000..09957ab --- /dev/null +++ b/codemods/legacy/jest/require-actual.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + interface Require { + requireActual(moduleName: string): unknown; + } +} diff --git a/codemods/legacy/jest/setup-require-actual.js b/codemods/legacy/jest/setup-require-actual.js new file mode 100644 index 0000000..81a0e55 --- /dev/null +++ b/codemods/legacy/jest/setup-require-actual.js @@ -0,0 +1,6 @@ +Object.defineProperty(Function.prototype, "requireActual", { + value: jest.requireActual, + configurable: true, + writable: true, +}); + diff --git a/codemods/legacy/package.json b/codemods/legacy/package.json new file mode 100644 index 0000000..09252c9 --- /dev/null +++ b/codemods/legacy/package.json @@ -0,0 +1,42 @@ +{ + "name": "@react-codemod-preview/legacy", + "version": "0.1.0", + "private": true, + "description": "Legacy jscodeshift snapshot for non-priority react-codemod transforms", + "scripts": { + "test": "jest --runInBand", + "test:ci": "jest --runInBand transforms/__tests__/class-test.js transforms/__tests__/custom-sort-group.js transforms/__tests__/custom-sort.js transforms/__tests__/error-boundaries.js transforms/__tests__/findDOMNode-test.js transforms/__tests__/pure-render-mixin-test.js transforms/__tests__/react-to-react-dom-test.js transforms/__tests__/React-DOM-to-react-dom-factories-test.js transforms/__tests__/sort-comp-test.js" + }, + "jest": { + "globals": { + "baseDir": "../../" + }, + "setupFilesAfterEnv": [ + "/jest/setup-require-actual.js" + ], + "testEnvironment": "node", + "roots": [ + "transforms" + ], + "transform": { + "^.+\\.jsx?$": "babel-jest", + "^.+\\.tsx?$": "ts-jest" + } + }, + "dependencies": { + "jscodeshift": "^0.11.0" + }, + "devDependencies": { + "@babel/core": "^7.6.4", + "@babel/plugin-proposal-object-rest-spread": "^7.6.2", + "@babel/preset-env": "^7.6.3", + "@types/jest": "^24.9.0", + "babel-eslint": "^10.0.3", + "babel-jest": "^24.9.0", + "eslint": "^6.6.0", + "eslint-plugin-react": "^7.16.0", + "jest": "^24.9.0", + "ts-jest": "^24.3.0", + "typescript": "4.8.4" + } +} diff --git a/codemods/legacy/pnpm-lock.yaml b/codemods/legacy/pnpm-lock.yaml new file mode 100644 index 0000000..72e1c10 --- /dev/null +++ b/codemods/legacy/pnpm-lock.yaml @@ -0,0 +1,6904 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + jscodeshift: + specifier: ^0.11.0 + version: 0.11.0(@babel/preset-env@7.29.2(@babel/core@7.29.0)) + devDependencies: + '@babel/core': + specifier: ^7.6.4 + version: 7.29.0 + '@babel/plugin-proposal-object-rest-spread': + specifier: ^7.6.2 + version: 7.20.7(@babel/core@7.29.0) + '@babel/preset-env': + specifier: ^7.6.3 + version: 7.29.2(@babel/core@7.29.0) + '@types/jest': + specifier: ^24.9.0 + version: 24.9.1 + babel-eslint: + specifier: ^10.0.3 + version: 10.1.0(eslint@6.8.0) + babel-jest: + specifier: ^24.9.0 + version: 24.9.0(@babel/core@7.29.0) + eslint: + specifier: ^6.6.0 + version: 6.8.0 + eslint-plugin-react: + specifier: ^7.16.0 + version: 7.37.5(eslint@6.8.0) + jest: + specifier: ^24.9.0 + version: 24.9.0 + ts-jest: + specifier: ^24.3.0 + version: 24.3.0(jest@24.9.0) + typescript: + specifier: 4.8.4 + version: 4.8.4 + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.6': + resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-create-regexp-features-plugin@7.28.5': + resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-define-polyfill-provider@0.6.8': + resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.28.5': + resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + + '@babel/helper-remap-async-to-generator@7.27.1': + resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-replace-supers@7.28.6': + resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-wrap-function@7.28.6': + resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': + resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': + resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': + resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': + resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': + resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-proposal-class-properties@7.18.6': + resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': + resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-object-rest-spread@7.20.7': + resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-optional-chaining@7.21.0': + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-flow@7.28.6': + resolution: {integrity: sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-assertions@7.28.6': + resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.28.6': + resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.28.6': + resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6': + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-arrow-functions@7.27.1': + resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-generator-functions@7.29.0': + resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-async-to-generator@7.28.6': + resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoped-functions@7.27.1': + resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-block-scoping@7.28.6': + resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-properties@7.28.6': + resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-class-static-block@7.28.6': + resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + + '@babel/plugin-transform-classes@7.28.6': + resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-computed-properties@7.28.6': + resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-destructuring@7.28.5': + resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-dotall-regex@7.28.6': + resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-keys@7.27.1': + resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-dynamic-import@7.27.1': + resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-explicit-resource-management@7.28.6': + resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-exponentiation-operator@7.28.6': + resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-export-namespace-from@7.27.1': + resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-flow-strip-types@7.27.1': + resolution: {integrity: sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-for-of@7.27.1': + resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-function-name@7.27.1': + resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-json-strings@7.28.6': + resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-literals@7.27.1': + resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6': + resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-member-expression-literals@7.27.1': + resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-amd@7.27.1': + resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-systemjs@7.29.0': + resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-umd@7.27.1': + resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': + resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-new-target@7.27.1': + resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': + resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-numeric-separator@7.28.6': + resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-rest-spread@7.28.6': + resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-object-super@7.27.1': + resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-catch-binding@7.28.6': + resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-optional-chaining@7.28.6': + resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-parameters@7.27.7': + resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-methods@7.28.6': + resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-private-property-in-object@7.28.6': + resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-property-literals@7.27.1': + resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regenerator@7.29.0': + resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-regexp-modifiers@7.28.6': + resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/plugin-transform-reserved-words@7.27.1': + resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-shorthand-properties@7.27.1': + resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-spread@7.28.6': + resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-sticky-regex@7.27.1': + resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-template-literals@7.27.1': + resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typeof-symbol@7.27.1': + resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.6': + resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-escapes@7.27.1': + resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-property-regex@7.28.6': + resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-regex@7.27.1': + resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6': + resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/preset-env@7.29.2': + resolution: {integrity: sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-flow@7.27.1': + resolution: {integrity: sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-modules@0.1.6-no-external-plugins': + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/register@7.28.6': + resolution: {integrity: sha512-pgcbbEl/dWQYb6L6Yew6F94rdwygfuv+vJ/tXfwIOYAfPB6TNWpXUMEtEq3YuTeHRdvMIhvz13bkT9CNaS+wqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@cnakazawa/watch@1.0.4': + resolution: {integrity: sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==} + engines: {node: '>=0.1.95'} + hasBin: true + + '@jest/console@24.9.0': + resolution: {integrity: sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==} + engines: {node: '>= 6'} + + '@jest/core@24.9.0': + resolution: {integrity: sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==} + engines: {node: '>= 6'} + + '@jest/environment@24.9.0': + resolution: {integrity: sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==} + engines: {node: '>= 6'} + + '@jest/fake-timers@24.9.0': + resolution: {integrity: sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==} + engines: {node: '>= 6'} + + '@jest/reporters@24.9.0': + resolution: {integrity: sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==} + engines: {node: '>= 6'} + + '@jest/source-map@24.9.0': + resolution: {integrity: sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==} + engines: {node: '>= 6'} + + '@jest/test-result@24.9.0': + resolution: {integrity: sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==} + engines: {node: '>= 6'} + + '@jest/test-sequencer@24.9.0': + resolution: {integrity: sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==} + engines: {node: '>= 6'} + + '@jest/transform@24.9.0': + resolution: {integrity: sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==} + engines: {node: '>= 6'} + + '@jest/types@24.9.0': + resolution: {integrity: sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==} + engines: {node: '>= 6'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@1.1.2': + resolution: {integrity: sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==} + + '@types/jest@24.9.1': + resolution: {integrity: sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==} + + '@types/stack-utils@1.0.1': + resolution: {integrity: sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@13.0.12': + resolution: {integrity: sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==} + + abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + + acorn-globals@4.3.4: + resolution: {integrity: sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@6.2.0: + resolution: {integrity: sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==} + engines: {node: '>=0.4.0'} + + acorn@5.7.4: + resolution: {integrity: sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@6.4.2: + resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.14.0: + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + + ansi-escapes@3.2.0: + resolution: {integrity: sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==} + engines: {node: '>=4'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@3.0.1: + resolution: {integrity: sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==} + engines: {node: '>=4'} + + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@2.0.0: + resolution: {integrity: sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + arr-diff@4.0.0: + resolution: {integrity: sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==} + engines: {node: '>=0.10.0'} + + arr-flatten@1.1.0: + resolution: {integrity: sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==} + engines: {node: '>=0.10.0'} + + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-equal@1.0.2: + resolution: {integrity: sha512-gUHx76KtnhEgB3HOuFYiCm3FIdEs6ocM2asHvNTkfu/Y09qQVrrVVaOKENmS2KkSaGoxgXNqC+ZVtR/n0MOkSA==} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array-unique@0.3.2: + resolution: {integrity: sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==} + engines: {node: '>=0.10.0'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.reduce@1.0.8: + resolution: {integrity: sha512-DwuEqgXFBwbmZSRqt3BpQigWNUoqw9Ml2dTWdF3B2zQlQX4OeUE0zyuzX0fX0IbTvjdkZbcBTU3idgpO78qkTw==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + + ast-types@0.14.2: + resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==} + engines: {node: '>=4'} + + astral-regex@1.0.0: + resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} + engines: {node: '>=4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async-limiter@1.0.1: + resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atob@2.1.2: + resolution: {integrity: sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==} + engines: {node: '>= 4.5.0'} + hasBin: true + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + babel-core@7.0.0-bridge.0: + resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + babel-eslint@10.1.0: + resolution: {integrity: sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==} + engines: {node: '>=6'} + deprecated: babel-eslint is now @babel/eslint-parser. This package will no longer receive updates. + peerDependencies: + eslint: '>= 4.12.1' + + babel-jest@24.9.0: + resolution: {integrity: sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==} + engines: {node: '>= 6'} + peerDependencies: + '@babel/core': ^7.0.0 + + babel-plugin-istanbul@5.2.0: + resolution: {integrity: sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==} + engines: {node: '>=6'} + + babel-plugin-jest-hoist@24.9.0: + resolution: {integrity: sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==} + engines: {node: '>= 6'} + + babel-plugin-polyfill-corejs2@0.4.17: + resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-corejs3@0.14.2: + resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-plugin-polyfill-regenerator@0.6.8: + resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + + babel-preset-jest@24.9.0: + resolution: {integrity: sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==} + engines: {node: '>= 6'} + peerDependencies: + '@babel/core': ^7.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base@0.11.2: + resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} + engines: {node: '>=0.10.0'} + + baseline-browser-mapping@2.10.19: + resolution: {integrity: sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + braces@2.3.2: + resolution: {integrity: sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==} + engines: {node: '>=0.10.0'} + + browser-process-hrtime@1.0.0: + resolution: {integrity: sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==} + + browser-resolve@1.11.3: + resolution: {integrity: sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + cache-base@1.0.1: + resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} + engines: {node: '>=0.10.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.9: + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@4.1.0: + resolution: {integrity: sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==} + engines: {node: '>=4'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001788: + resolution: {integrity: sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==} + + capture-exit@2.0.0: + resolution: {integrity: sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==} + engines: {node: 6.* || 8.* || >= 10.*} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + ci-info@2.0.0: + resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} + + class-utils@0.3.6: + resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} + engines: {node: '>=0.10.0'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + cliui@5.0.0: + resolution: {integrity: sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==} + + clone-deep@4.0.1: + resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} + engines: {node: '>=6'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collection-visit@1.0.0: + resolution: {integrity: sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==} + engines: {node: '>=0.10.0'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-descriptor@0.1.1: + resolution: {integrity: sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==} + engines: {node: '>=0.10.0'} + + core-js-compat@3.49.0: + resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + cross-spawn@6.0.6: + resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} + engines: {node: '>=4.8'} + + cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + + cssstyle@1.4.0: + resolution: {integrity: sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==} + + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + data-urls@1.1.0: + resolution: {integrity: sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + define-property@0.2.5: + resolution: {integrity: sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==} + engines: {node: '>=0.10.0'} + + define-property@1.0.0: + resolution: {integrity: sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==} + engines: {node: '>=0.10.0'} + + define-property@2.0.2: + resolution: {integrity: sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==} + engines: {node: '>=0.10.0'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-newline@2.1.0: + resolution: {integrity: sha512-CwffZFvlJffUg9zZA0uqrjQayUTC8ob94pnr5sFwaVv3IOmkfUHcWH+jXaQK3askE51Cqe8/9Ql/0uXNwqZ8Zg==} + engines: {node: '>=0.10.0'} + + diff-sequences@24.9.0: + resolution: {integrity: sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==} + engines: {node: '>= 6'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + + domexception@1.0.1: + resolution: {integrity: sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==} + deprecated: Use your platform's native DOMException instead + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + electron-to-chromium@1.5.336: + resolution: {integrity: sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ==} + + emoji-regex@7.0.3: + resolution: {integrity: sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-abstract@1.24.2: + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} + + es-array-method-boxes-properly@1.0.0: + resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.3.2: + resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escodegen@1.14.3: + resolution: {integrity: sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==} + engines: {node: '>=4.0'} + hasBin: true + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-utils@1.4.3: + resolution: {integrity: sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==} + engines: {node: '>=6'} + + eslint-visitor-keys@1.3.0: + resolution: {integrity: sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==} + engines: {node: '>=4'} + + eslint@6.8.0: + resolution: {integrity: sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==} + engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + + espree@6.2.1: + resolution: {integrity: sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==} + engines: {node: '>=6.0.0'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + exec-sh@0.3.6: + resolution: {integrity: sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==} + + execa@1.0.0: + resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} + engines: {node: '>=6'} + + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + expand-brackets@2.1.4: + resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} + engines: {node: '>=0.10.0'} + + expect@24.9.0: + resolution: {integrity: sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==} + engines: {node: '>= 6'} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + extglob@2.0.4: + resolution: {integrity: sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==} + engines: {node: '>=0.10.0'} + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-entry-cache@5.0.1: + resolution: {integrity: sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==} + engines: {node: '>=4'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fill-range@4.0.0: + resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} + engines: {node: '>=0.10.0'} + + find-cache-dir@2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + flat-cache@2.0.1: + resolution: {integrity: sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==} + engines: {node: '>=4'} + + flatted@2.0.2: + resolution: {integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==} + + flow-parser@0.309.0: + resolution: {integrity: sha512-poYRskeIXiHsE19Fb9sRE/CV7PYOq21j3lS5vKr27ujFBvSAhmCbbilAonJ0/u0Uai+Xgyq30/twHQeQc2Ngiw==} + engines: {node: '>=0.4.0'} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + for-in@1.0.2: + resolution: {integrity: sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==} + engines: {node: '>=0.10.0'} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data@2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + + fragment-cache@0.2.1: + resolution: {integrity: sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==} + engines: {node: '>=0.10.0'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@1.2.13: + resolution: {integrity: sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==} + engines: {node: '>= 4.0'} + os: [darwin] + deprecated: Upgrade to fsevents v2 to mitigate potential security issues + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functional-red-black-tree@1.0.1: + resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + generator-function@2.0.1: + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + globals@12.4.0: + resolution: {integrity: sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==} + engines: {node: '>=8'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + growly@1.3.0: + resolution: {integrity: sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw==} + + har-schema@2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + + har-validator@5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + has-value@0.3.1: + resolution: {integrity: sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==} + engines: {node: '>=0.10.0'} + + has-value@1.0.0: + resolution: {integrity: sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==} + engines: {node: '>=0.10.0'} + + has-values@0.1.4: + resolution: {integrity: sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==} + engines: {node: '>=0.10.0'} + + has-values@1.0.0: + resolution: {integrity: sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==} + engines: {node: '>=0.10.0'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hosted-git-info@2.8.9: + resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + + html-encoding-sniffer@1.0.2: + resolution: {integrity: sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-signature@1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + ignore@4.0.6: + resolution: {integrity: sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@2.0.0: + resolution: {integrity: sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==} + engines: {node: '>=6'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + inquirer@7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + is-accessor-descriptor@1.0.1: + resolution: {integrity: sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==} + engines: {node: '>= 0.10'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-ci@2.0.0: + resolution: {integrity: sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==} + hasBin: true + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-descriptor@1.0.1: + resolution: {integrity: sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-descriptor@0.1.7: + resolution: {integrity: sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==} + engines: {node: '>= 0.4'} + + is-descriptor@1.0.3: + resolution: {integrity: sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==} + engines: {node: '>= 0.4'} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@2.0.0: + resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} + engines: {node: '>=4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-generator-function@1.1.2: + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@3.0.0: + resolution: {integrity: sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==} + engines: {node: '>=0.10.0'} + + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-stream@1.1.0: + resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} + engines: {node: '>=0.10.0'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + is-wsl@1.1.0: + resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} + engines: {node: '>=4'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isobject@2.1.0: + resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} + engines: {node: '>=0.10.0'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + istanbul-lib-coverage@2.0.5: + resolution: {integrity: sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==} + engines: {node: '>=6'} + + istanbul-lib-instrument@3.3.0: + resolution: {integrity: sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==} + engines: {node: '>=6'} + + istanbul-lib-report@2.0.8: + resolution: {integrity: sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==} + engines: {node: '>=6'} + + istanbul-lib-source-maps@3.0.6: + resolution: {integrity: sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==} + engines: {node: '>=6'} + + istanbul-reports@2.2.7: + resolution: {integrity: sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==} + engines: {node: '>=6'} + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + jest-changed-files@24.9.0: + resolution: {integrity: sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==} + engines: {node: '>= 6'} + + jest-cli@24.9.0: + resolution: {integrity: sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==} + engines: {node: '>= 6'} + hasBin: true + + jest-config@24.9.0: + resolution: {integrity: sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==} + engines: {node: '>= 6'} + + jest-diff@24.9.0: + resolution: {integrity: sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==} + engines: {node: '>= 6'} + + jest-docblock@24.9.0: + resolution: {integrity: sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==} + engines: {node: '>= 6'} + + jest-each@24.9.0: + resolution: {integrity: sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==} + engines: {node: '>= 6'} + + jest-environment-jsdom@24.9.0: + resolution: {integrity: sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==} + engines: {node: '>= 6'} + + jest-environment-node@24.9.0: + resolution: {integrity: sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==} + engines: {node: '>= 6'} + + jest-get-type@24.9.0: + resolution: {integrity: sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==} + engines: {node: '>= 6'} + + jest-haste-map@24.9.0: + resolution: {integrity: sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==} + engines: {node: '>= 6'} + + jest-jasmine2@24.9.0: + resolution: {integrity: sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==} + engines: {node: '>= 6'} + + jest-leak-detector@24.9.0: + resolution: {integrity: sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==} + engines: {node: '>= 6'} + + jest-matcher-utils@24.9.0: + resolution: {integrity: sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==} + engines: {node: '>= 6'} + + jest-message-util@24.9.0: + resolution: {integrity: sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==} + engines: {node: '>= 6'} + + jest-mock@24.9.0: + resolution: {integrity: sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==} + engines: {node: '>= 6'} + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-regex-util@24.9.0: + resolution: {integrity: sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==} + engines: {node: '>= 6'} + + jest-resolve-dependencies@24.9.0: + resolution: {integrity: sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==} + engines: {node: '>= 6'} + + jest-resolve@24.9.0: + resolution: {integrity: sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==} + engines: {node: '>= 6'} + + jest-runner@24.9.0: + resolution: {integrity: sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==} + engines: {node: '>= 6'} + + jest-runtime@24.9.0: + resolution: {integrity: sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==} + engines: {node: '>= 6'} + hasBin: true + + jest-serializer@24.9.0: + resolution: {integrity: sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==} + engines: {node: '>= 6'} + + jest-snapshot@24.9.0: + resolution: {integrity: sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==} + engines: {node: '>= 6'} + + jest-util@24.9.0: + resolution: {integrity: sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==} + engines: {node: '>= 6'} + + jest-validate@24.9.0: + resolution: {integrity: sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==} + engines: {node: '>= 6'} + + jest-watcher@24.9.0: + resolution: {integrity: sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==} + engines: {node: '>= 6'} + + jest-worker@24.9.0: + resolution: {integrity: sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==} + engines: {node: '>= 6'} + + jest@24.9.0: + resolution: {integrity: sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==} + engines: {node: '>= 6'} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + jscodeshift@0.11.0: + resolution: {integrity: sha512-SdRK2C7jjs4k/kT2mwtO07KJN9RnjxtKn03d9JVj6c3j9WwaLcFYsICYDnLAzY0hp+wG2nxl+Cm2jWLiNVYb8g==} + hasBin: true + peerDependencies: + '@babel/preset-env': ^7.1.6 + + jsdom@11.12.0: + resolution: {integrity: sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-parse-better-errors@1.0.2: + resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsprim@1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + kind-of@3.2.2: + resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==} + engines: {node: '>=0.10.0'} + + kind-of@4.0.0: + resolution: {integrity: sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==} + engines: {node: '>=0.10.0'} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + left-pad@1.3.0: + resolution: {integrity: sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==} + deprecated: use String.prototype.padStart() + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.3.0: + resolution: {integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==} + engines: {node: '>= 0.8.0'} + + load-json-file@4.0.0: + resolution: {integrity: sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==} + engines: {node: '>=4'} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + make-dir@2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + + map-cache@0.2.2: + resolution: {integrity: sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==} + engines: {node: '>=0.10.0'} + + map-visit@1.0.0: + resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==} + engines: {node: '>=0.10.0'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + micromatch@3.1.10: + resolution: {integrity: sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==} + engines: {node: '>=0.10.0'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mixin-deep@1.3.2: + resolution: {integrity: sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==} + engines: {node: '>=0.10.0'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + nan@2.26.2: + resolution: {integrity: sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==} + + nanomatch@1.2.13: + resolution: {integrity: sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==} + engines: {node: '>=0.10.0'} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + nice-try@1.0.5: + resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + + node-dir@0.1.17: + resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} + engines: {node: '>= 0.10.5'} + + node-exports-info@1.6.0: + resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} + engines: {node: '>= 0.4'} + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-notifier@5.4.5: + resolution: {integrity: sha512-tVbHs7DyTLtzOiN78izLA85zRqB9NvEXkAf014Vx3jtSvn/xBl6bR8ZYifj+dFcFrKI21huSQgJZ6ZtL3B4HfQ==} + + node-releases@2.0.37: + resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==} + + normalize-package-data@2.5.0: + resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} + + normalize-path@2.1.1: + resolution: {integrity: sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==} + engines: {node: '>=0.10.0'} + + npm-run-path@2.0.2: + resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} + engines: {node: '>=4'} + + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + + oauth-sign@0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-copy@0.1.0: + resolution: {integrity: sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object-visit@1.0.1: + resolution: {integrity: sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==} + engines: {node: '>=0.10.0'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.getownpropertydescriptors@2.1.9: + resolution: {integrity: sha512-mt8YM6XwsTTovI+kdZdHSxoyF2DI59up034orlC9NfweclcWOt7CVascNNLp6U+bjFVCVCIh9PwS76tDM/rH8g==} + engines: {node: '>= 0.4'} + + object.pick@1.3.0: + resolution: {integrity: sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==} + engines: {node: '>=0.10.0'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.8.3: + resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} + engines: {node: '>= 0.8.0'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-each-series@1.0.0: + resolution: {integrity: sha512-J/e9xiZZQNrt+958FFzJ+auItsBGq+UrQ7nE89AUP7UOTtjHnkISANXLdayhVzh538UnLMCSlf13lFfRIAKQOA==} + engines: {node: '>=4'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-reduce@1.0.0: + resolution: {integrity: sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==} + engines: {node: '>=4'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@4.0.0: + resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} + engines: {node: '>=4'} + + parse5@4.0.0: + resolution: {integrity: sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==} + + pascalcase@0.1.1: + resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} + engines: {node: '>=0.10.0'} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@2.0.1: + resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} + engines: {node: '>=4'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@3.0.0: + resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} + engines: {node: '>=4'} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pify@3.0.0: + resolution: {integrity: sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==} + engines: {node: '>=4'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + + pn@1.1.0: + resolution: {integrity: sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==} + + posix-character-classes@0.1.1: + resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} + engines: {node: '>=0.10.0'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + prelude-ls@1.1.2: + resolution: {integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==} + engines: {node: '>= 0.8.0'} + + pretty-format@24.9.0: + resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} + engines: {node: '>= 6'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.5.5: + resolution: {integrity: sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==} + engines: {node: '>=0.6'} + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + read-pkg-up@4.0.0: + resolution: {integrity: sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==} + engines: {node: '>=6'} + + read-pkg@3.0.0: + resolution: {integrity: sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==} + engines: {node: '>=4'} + + realpath-native@1.1.0: + resolution: {integrity: sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==} + engines: {node: '>=4'} + + recast@0.20.5: + resolution: {integrity: sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==} + engines: {node: '>= 4'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regenerate-unicode-properties@10.2.2: + resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + engines: {node: '>=4'} + + regenerate@1.4.2: + resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} + + regex-not@1.0.2: + resolution: {integrity: sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==} + engines: {node: '>=0.10.0'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + regexpp@2.0.1: + resolution: {integrity: sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==} + engines: {node: '>=6.5.0'} + + regexpu-core@6.4.0: + resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + engines: {node: '>=4'} + + regjsgen@0.8.0: + resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} + + regjsparser@0.13.1: + resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} + hasBin: true + + remove-trailing-separator@1.1.0: + resolution: {integrity: sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==} + + repeat-element@1.1.4: + resolution: {integrity: sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==} + engines: {node: '>=0.10.0'} + + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + + request-promise-core@1.1.4: + resolution: {integrity: sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==} + engines: {node: '>=0.10.0'} + peerDependencies: + request: ^2.34 + + request-promise-native@1.0.9: + resolution: {integrity: sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==} + engines: {node: '>=0.12.0'} + deprecated: request-promise-native has been deprecated because it extends the now deprecated request package, see https://github.com/request/request/issues/3142 + peerDependencies: + request: ^2.34 + + request@2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + + resolve-cwd@2.0.0: + resolution: {integrity: sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==} + engines: {node: '>=4'} + + resolve-from@3.0.0: + resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} + engines: {node: '>=4'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-url@0.2.1: + resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} + deprecated: https://github.com/lydell/resolve-url#deprecated + + resolve@1.1.7: + resolution: {integrity: sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.6: + resolution: {integrity: sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + ret@0.1.15: + resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} + engines: {node: '>=0.12'} + + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rsvp@4.8.5: + resolution: {integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==} + engines: {node: 6.* || >= 7.*} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-regex@1.1.0: + resolution: {integrity: sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sane@4.1.0: + resolution: {integrity: sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==} + engines: {node: 6.* || 8.* || >= 10.*} + deprecated: some dependency vulnerabilities fixed, support for node < 10 dropped, and newer ECMAScript syntax/features added + hasBin: true + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + + shallow-clone@3.0.1: + resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} + engines: {node: '>=8'} + + shebang-command@1.2.0: + resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} + engines: {node: '>=0.10.0'} + + shebang-regex@1.0.0: + resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} + engines: {node: '>=0.10.0'} + + shellwords@0.1.1: + resolution: {integrity: sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slash@2.0.0: + resolution: {integrity: sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==} + engines: {node: '>=6'} + + slice-ansi@2.1.0: + resolution: {integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==} + engines: {node: '>=6'} + + snapdragon-node@2.1.1: + resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==} + engines: {node: '>=0.10.0'} + + snapdragon-util@3.0.1: + resolution: {integrity: sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==} + engines: {node: '>=0.10.0'} + + snapdragon@0.8.2: + resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} + engines: {node: '>=0.10.0'} + + source-map-resolve@0.5.3: + resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} + deprecated: See https://github.com/lydell/source-map-resolve#deprecated + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map-url@0.4.1: + resolution: {integrity: sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==} + deprecated: See https://github.com/lydell/source-map-url#deprecated + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + spdx-correct@3.2.0: + resolution: {integrity: sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==} + + spdx-exceptions@2.5.0: + resolution: {integrity: sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==} + + spdx-expression-parse@3.0.1: + resolution: {integrity: sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==} + + spdx-license-ids@3.0.23: + resolution: {integrity: sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==} + + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + stack-utils@1.0.5: + resolution: {integrity: sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==} + engines: {node: '>=8'} + + static-extend@0.1.2: + resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} + engines: {node: '>=0.10.0'} + + stealthy-require@1.1.1: + resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} + engines: {node: '>=0.10.0'} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string-length@2.0.0: + resolution: {integrity: sha512-Qka42GGrS8Mm3SZ+7cH8UXiIWI867/b/Z/feQSpQx/rbfB8UGknGEZVaUQMOUVj+soY6NpWAxily63HI1OckVQ==} + engines: {node: '>=4'} + + string-width@3.1.0: + resolution: {integrity: sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==} + engines: {node: '>=6'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-ansi@4.0.0: + resolution: {integrity: sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==} + engines: {node: '>=4'} + + strip-ansi@5.2.0: + resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} + engines: {node: '>=6'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-eof@1.0.0: + resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@6.1.0: + resolution: {integrity: sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==} + engines: {node: '>=6'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + table@5.4.6: + resolution: {integrity: sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==} + engines: {node: '>=6.0.0'} + + temp@0.8.4: + resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==} + engines: {node: '>=6.0.0'} + + test-exclude@5.2.3: + resolution: {integrity: sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==} + engines: {node: '>=6'} + + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + + throat@4.1.0: + resolution: {integrity: sha512-wCVxLDcFxw7ujDxaeJC6nfl2XfHJNYs8yUYJnvMgtPEFlttP9tHSfRUv2vBe6C4hkVFPWoP1P6ZccbYjmSEkKA==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + to-object-path@0.3.0: + resolution: {integrity: sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==} + engines: {node: '>=0.10.0'} + + to-regex-range@2.1.1: + resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} + engines: {node: '>=0.10.0'} + + to-regex@3.0.2: + resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==} + engines: {node: '>=0.10.0'} + + tough-cookie@2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + + ts-jest@24.3.0: + resolution: {integrity: sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ==} + engines: {node: '>= 6'} + hasBin: true + peerDependencies: + jest: '>=24 <25' + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-check@0.3.2: + resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} + engines: {node: '>= 0.8.0'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript@4.8.4: + resolution: {integrity: sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==} + engines: {node: '>=4.2.0'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + unicode-canonical-property-names-ecmascript@2.0.1: + resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} + engines: {node: '>=4'} + + unicode-match-property-ecmascript@2.0.0: + resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} + engines: {node: '>=4'} + + unicode-match-property-value-ecmascript@2.2.1: + resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + engines: {node: '>=4'} + + unicode-property-aliases-ecmascript@2.2.0: + resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + engines: {node: '>=4'} + + union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + + unset-value@1.0.0: + resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} + engines: {node: '>=0.10.0'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + urix@0.1.0: + resolution: {integrity: sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==} + deprecated: Please see https://github.com/lydell/urix#deprecated + + use@3.1.1: + resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==} + engines: {node: '>=0.10.0'} + + util.promisify@1.1.3: + resolution: {integrity: sha512-GIEaZ6o86fj09Wtf0VfZ5XP7tmd4t3jM5aZCgmBi231D0DB1AEBa3Aa6MP48DMsAIi96WkpWLimIWVwOjbDMOw==} + engines: {node: '>= 0.8'} + + uuid@3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + + v8-compile-cache@2.4.0: + resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} + + validate-npm-package-license@3.0.4: + resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + + w3c-hr-time@1.0.2: + resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} + deprecated: Use your platform's native performance.now() and performance.timeOrigin. + + walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + + whatwg-encoding@1.0.5: + resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@2.3.0: + resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==} + + whatwg-url@6.5.0: + resolution: {integrity: sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==} + + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + + which-typed-array@1.1.20: + resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + engines: {node: '>= 0.4'} + + which@1.3.1: + resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@5.1.0: + resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} + engines: {node: '>=6'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@2.4.1: + resolution: {integrity: sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==} + + write-file-atomic@2.4.3: + resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} + + write@1.0.3: + resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==} + engines: {node: '>=4'} + + ws@5.2.4: + resolution: {integrity: sha512-fFCejsuC8f9kOSu9FYaOw8CdO68O3h5v0lg4p74o8JqWpwTf9tniOD+nOB78aWoVSS6WptVUmDrp/KPsMVBWFQ==} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@3.0.0: + resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@10.1.0: + resolution: {integrity: sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==} + + yargs-parser@13.1.2: + resolution: {integrity: sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==} + + yargs@13.3.2: + resolution: {integrity: sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==} + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.29.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + regexpu-core: 6.4.0 + semver: 6.3.1 + + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + debug: 4.4.3 + lodash.debounce: 4.0.8 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.28.5': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.29.0 + + '@babel/helper-plugin-utils@7.28.6': {} + + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-wrap-function': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helper-wrap-function@7.28.6': + dependencies: + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + + '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.29.0)': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + + '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + + '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/template': 7.28.6 + + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/preset-env@7.29.2(@babel/core@7.29.0)': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) + '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/preset-flow@7.27.1(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) + + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/types': 7.29.0 + esutils: 2.0.3 + + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + '@babel/register@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + clone-deep: 4.0.1 + find-cache-dir: 2.1.0 + make-dir: 2.1.0 + pirates: 4.0.7 + source-map-support: 0.5.21 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@cnakazawa/watch@1.0.4': + dependencies: + exec-sh: 0.3.6 + minimist: 1.2.8 + + '@jest/console@24.9.0': + dependencies: + '@jest/source-map': 24.9.0 + chalk: 2.4.2 + slash: 2.0.0 + + '@jest/core@24.9.0': + dependencies: + '@jest/console': 24.9.0 + '@jest/reporters': 24.9.0 + '@jest/test-result': 24.9.0 + '@jest/transform': 24.9.0 + '@jest/types': 24.9.0 + ansi-escapes: 3.2.0 + chalk: 2.4.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 24.9.0 + jest-config: 24.9.0 + jest-haste-map: 24.9.0 + jest-message-util: 24.9.0 + jest-regex-util: 24.9.0 + jest-resolve: 24.9.0 + jest-resolve-dependencies: 24.9.0 + jest-runner: 24.9.0 + jest-runtime: 24.9.0 + jest-snapshot: 24.9.0 + jest-util: 24.9.0 + jest-validate: 24.9.0 + jest-watcher: 24.9.0 + micromatch: 3.1.10 + p-each-series: 1.0.0 + realpath-native: 1.1.0 + rimraf: 2.7.1 + slash: 2.0.0 + strip-ansi: 5.2.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@jest/environment@24.9.0': + dependencies: + '@jest/fake-timers': 24.9.0 + '@jest/transform': 24.9.0 + '@jest/types': 24.9.0 + jest-mock: 24.9.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@24.9.0': + dependencies: + '@jest/types': 24.9.0 + jest-message-util: 24.9.0 + jest-mock: 24.9.0 + transitivePeerDependencies: + - supports-color + + '@jest/reporters@24.9.0': + dependencies: + '@jest/environment': 24.9.0 + '@jest/test-result': 24.9.0 + '@jest/transform': 24.9.0 + '@jest/types': 24.9.0 + chalk: 2.4.2 + exit: 0.1.2 + glob: 7.2.3 + istanbul-lib-coverage: 2.0.5 + istanbul-lib-instrument: 3.3.0 + istanbul-lib-report: 2.0.8 + istanbul-lib-source-maps: 3.0.6 + istanbul-reports: 2.2.7 + jest-haste-map: 24.9.0 + jest-resolve: 24.9.0 + jest-runtime: 24.9.0 + jest-util: 24.9.0 + jest-worker: 24.9.0 + node-notifier: 5.4.5 + slash: 2.0.0 + source-map: 0.6.1 + string-length: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@jest/source-map@24.9.0': + dependencies: + callsites: 3.1.0 + graceful-fs: 4.2.11 + source-map: 0.6.1 + + '@jest/test-result@24.9.0': + dependencies: + '@jest/console': 24.9.0 + '@jest/types': 24.9.0 + '@types/istanbul-lib-coverage': 2.0.6 + + '@jest/test-sequencer@24.9.0': + dependencies: + '@jest/test-result': 24.9.0 + jest-haste-map: 24.9.0 + jest-runner: 24.9.0 + jest-runtime: 24.9.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@jest/transform@24.9.0': + dependencies: + '@babel/core': 7.29.0 + '@jest/types': 24.9.0 + babel-plugin-istanbul: 5.2.0 + chalk: 2.4.2 + convert-source-map: 1.9.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 24.9.0 + jest-regex-util: 24.9.0 + jest-util: 24.9.0 + micromatch: 3.1.10 + pirates: 4.0.7 + realpath-native: 1.1.0 + slash: 2.0.0 + source-map: 0.6.1 + write-file-atomic: 2.4.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@24.9.0': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 1.1.2 + '@types/yargs': 13.0.12 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.0 + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@1.1.2': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@24.9.1': + dependencies: + jest-diff: 24.9.0 + + '@types/stack-utils@1.0.1': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@13.0.12': + dependencies: + '@types/yargs-parser': 21.0.3 + + abab@2.0.6: {} + + acorn-globals@4.3.4: + dependencies: + acorn: 6.4.2 + acorn-walk: 6.2.0 + + acorn-jsx@5.3.2(acorn@7.4.1): + dependencies: + acorn: 7.4.1 + + acorn-walk@6.2.0: {} + + acorn@5.7.4: {} + + acorn@6.4.2: {} + + acorn@7.4.1: {} + + ajv@6.14.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@3.2.0: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@3.0.1: {} + + ansi-regex@4.1.1: {} + + ansi-regex@5.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@2.0.0: + dependencies: + micromatch: 3.1.10 + normalize-path: 2.1.1 + transitivePeerDependencies: + - supports-color + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + arr-diff@4.0.0: {} + + arr-flatten@1.1.0: {} + + arr-union@3.1.0: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-equal@1.0.2: {} + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array-unique@0.3.2: {} + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-shim-unscopables: 1.1.0 + + array.prototype.reduce@1.0.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-array-method-boxes-properly: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + is-string: 1.1.1 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + + assign-symbols@1.0.0: {} + + ast-types@0.14.2: + dependencies: + tslib: 2.8.1 + + astral-regex@1.0.0: {} + + async-function@1.0.0: {} + + async-limiter@1.0.1: {} + + asynckit@0.4.0: {} + + atob@2.1.2: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + + babel-core@7.0.0-bridge.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + + babel-eslint@10.1.0(eslint@6.8.0): + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + eslint: 6.8.0 + eslint-visitor-keys: 1.3.0 + resolve: 1.22.12 + transitivePeerDependencies: + - supports-color + + babel-jest@24.9.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@jest/transform': 24.9.0 + '@jest/types': 24.9.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 5.2.0 + babel-preset-jest: 24.9.0(@babel/core@7.29.0) + chalk: 2.4.2 + slash: 2.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@5.2.0: + dependencies: + '@babel/helper-plugin-utils': 7.28.6 + find-up: 3.0.0 + istanbul-lib-instrument: 3.3.0 + test-exclude: 5.2.3 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@24.9.0: + dependencies: + '@types/babel__traverse': 7.28.0 + + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + core-js-compat: 3.49.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + transitivePeerDependencies: + - supports-color + + babel-preset-jest@24.9.0(@babel/core@7.29.0): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + babel-plugin-jest-hoist: 24.9.0 + + balanced-match@1.0.2: {} + + base@0.11.2: + dependencies: + cache-base: 1.0.1 + class-utils: 0.3.6 + component-emitter: 1.3.1 + define-property: 1.0.0 + isobject: 3.0.1 + mixin-deep: 1.3.2 + pascalcase: 0.1.1 + + baseline-browser-mapping@2.10.19: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + optional: true + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + braces@2.3.2: + dependencies: + arr-flatten: 1.1.0 + array-unique: 0.3.2 + extend-shallow: 2.0.1 + fill-range: 4.0.0 + isobject: 3.0.1 + repeat-element: 1.1.4 + snapdragon: 0.8.2 + snapdragon-node: 2.1.1 + split-string: 3.1.0 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + browser-process-hrtime@1.0.0: {} + + browser-resolve@1.11.3: + dependencies: + resolve: 1.1.7 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.19 + caniuse-lite: 1.0.30001788 + electron-to-chromium: 1.5.336 + node-releases: 2.0.37 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-from@1.1.2: {} + + cache-base@1.0.1: + dependencies: + collection-visit: 1.0.0 + component-emitter: 1.3.1 + get-value: 2.0.6 + has-value: 1.0.0 + isobject: 3.0.1 + set-value: 2.0.1 + to-object-path: 0.3.0 + union-value: 1.0.1 + unset-value: 1.0.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.9: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@4.1.0: {} + + camelcase@5.3.1: {} + + caniuse-lite@1.0.30001788: {} + + capture-exit@2.0.0: + dependencies: + rsvp: 4.8.5 + + caseless@0.12.0: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chardet@0.7.0: {} + + ci-info@2.0.0: {} + + class-utils@0.3.6: + dependencies: + arr-union: 3.1.0 + define-property: 0.2.5 + isobject: 3.0.1 + static-extend: 0.1.2 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-width@3.0.0: {} + + cliui@5.0.0: + dependencies: + string-width: 3.1.0 + strip-ansi: 5.2.0 + wrap-ansi: 5.1.0 + + clone-deep@4.0.1: + dependencies: + is-plain-object: 2.0.4 + kind-of: 6.0.3 + shallow-clone: 3.0.1 + + co@4.6.0: {} + + collection-visit@1.0.0: + dependencies: + map-visit: 1.0.0 + object-visit: 1.0.1 + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + colors@1.4.0: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commondir@1.0.1: {} + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + copy-descriptor@0.1.1: {} + + core-js-compat@3.49.0: + dependencies: + browserslist: 4.28.2 + + core-util-is@1.0.2: {} + + cross-spawn@6.0.6: + dependencies: + nice-try: 1.0.5 + path-key: 2.0.1 + semver: 5.7.2 + shebang-command: 1.2.0 + which: 1.3.1 + + cssom@0.3.8: {} + + cssstyle@1.4.0: + dependencies: + cssom: 0.3.8 + + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + + data-urls@1.1.0: + dependencies: + abab: 2.0.6 + whatwg-mimetype: 2.3.0 + whatwg-url: 7.1.0 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + decode-uri-component@0.2.2: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + define-property@0.2.5: + dependencies: + is-descriptor: 0.1.7 + + define-property@1.0.0: + dependencies: + is-descriptor: 1.0.3 + + define-property@2.0.2: + dependencies: + is-descriptor: 1.0.3 + isobject: 3.0.1 + + delayed-stream@1.0.0: {} + + detect-newline@2.1.0: {} + + diff-sequences@24.9.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + + domexception@1.0.1: + dependencies: + webidl-conversions: 4.0.2 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + electron-to-chromium@1.5.336: {} + + emoji-regex@7.0.3: {} + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-abstract@1.24.2: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.20 + + es-array-method-boxes-properly@1.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.3.2: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + math-intrinsics: 1.1.0 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + escalade@3.2.0: {} + + escape-string-regexp@1.0.5: {} + + escape-string-regexp@2.0.0: {} + + escodegen@1.14.3: + dependencies: + esprima: 4.0.1 + estraverse: 4.3.0 + esutils: 2.0.3 + optionator: 0.8.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-plugin-react@7.37.5(eslint@6.8.0): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.3.2 + eslint: 6.8.0 + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.5 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.6 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-utils@1.4.3: + dependencies: + eslint-visitor-keys: 1.3.0 + + eslint-visitor-keys@1.3.0: {} + + eslint@6.8.0: + dependencies: + '@babel/code-frame': 7.29.0 + ajv: 6.14.0 + chalk: 2.4.2 + cross-spawn: 6.0.6 + debug: 4.4.3 + doctrine: 3.0.0 + eslint-scope: 5.1.1 + eslint-utils: 1.4.3 + eslint-visitor-keys: 1.3.0 + espree: 6.2.1 + esquery: 1.7.0 + esutils: 2.0.3 + file-entry-cache: 5.0.1 + functional-red-black-tree: 1.0.1 + glob-parent: 5.1.2 + globals: 12.4.0 + ignore: 4.0.6 + import-fresh: 3.3.1 + imurmurhash: 0.1.4 + inquirer: 7.3.3 + is-glob: 4.0.3 + js-yaml: 3.14.2 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.3.0 + lodash: 4.18.1 + minimatch: 3.1.5 + mkdirp: 0.5.6 + natural-compare: 1.4.0 + optionator: 0.8.3 + progress: 2.0.3 + regexpp: 2.0.1 + semver: 6.3.1 + strip-ansi: 5.2.0 + strip-json-comments: 3.1.1 + table: 5.4.6 + text-table: 0.2.0 + v8-compile-cache: 2.4.0 + transitivePeerDependencies: + - supports-color + + espree@6.2.1: + dependencies: + acorn: 7.4.1 + acorn-jsx: 5.3.2(acorn@7.4.1) + eslint-visitor-keys: 1.3.0 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + exec-sh@0.3.6: {} + + execa@1.0.0: + dependencies: + cross-spawn: 6.0.6 + get-stream: 4.1.0 + is-stream: 1.1.0 + npm-run-path: 2.0.2 + p-finally: 1.0.0 + signal-exit: 3.0.7 + strip-eof: 1.0.0 + + exit@0.1.2: {} + + expand-brackets@2.1.4: + dependencies: + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + posix-character-classes: 0.1.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + expect@24.9.0: + dependencies: + '@jest/types': 24.9.0 + ansi-styles: 3.2.1 + jest-get-type: 24.9.0 + jest-matcher-utils: 24.9.0 + jest-message-util: 24.9.0 + jest-regex-util: 24.9.0 + transitivePeerDependencies: + - supports-color + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + + extend@3.0.2: {} + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + extglob@2.0.4: + dependencies: + array-unique: 0.3.2 + define-property: 1.0.0 + expand-brackets: 2.1.4 + extend-shallow: 2.0.1 + fragment-cache: 0.2.1 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + extsprintf@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-entry-cache@5.0.1: + dependencies: + flat-cache: 2.0.1 + + file-uri-to-path@1.0.0: + optional: true + + fill-range@4.0.0: + dependencies: + extend-shallow: 2.0.1 + is-number: 3.0.0 + repeat-string: 1.6.1 + to-regex-range: 2.1.1 + + find-cache-dir@2.1.0: + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + flat-cache@2.0.1: + dependencies: + flatted: 2.0.2 + rimraf: 2.6.3 + write: 1.0.3 + + flatted@2.0.2: {} + + flow-parser@0.309.0: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + for-in@1.0.2: {} + + forever-agent@0.6.1: {} + + form-data@2.3.3: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + fragment-cache@0.2.1: + dependencies: + map-cache: 0.2.2 + + fs.realpath@1.0.0: {} + + fsevents@1.2.13: + dependencies: + bindings: 1.5.0 + nan: 2.26.2 + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functional-red-black-tree@1.0.1: {} + + functions-have-names@1.2.3: {} + + generator-function@2.0.1: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@4.1.0: + dependencies: + pump: 3.0.4 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + get-value@2.0.6: {} + + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@12.4.0: + dependencies: + type-fest: 0.8.1 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + growly@1.3.0: {} + + har-schema@2.0.0: {} + + har-validator@5.1.5: + dependencies: + ajv: 6.14.0 + har-schema: 2.0.0 + + has-bigints@1.1.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + has-value@0.3.1: + dependencies: + get-value: 2.0.6 + has-values: 0.1.4 + isobject: 2.1.0 + + has-value@1.0.0: + dependencies: + get-value: 2.0.6 + has-values: 1.0.0 + isobject: 3.0.1 + + has-values@0.1.4: {} + + has-values@1.0.0: + dependencies: + is-number: 3.0.0 + kind-of: 4.0.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@2.8.9: {} + + html-encoding-sniffer@1.0.2: + dependencies: + whatwg-encoding: 1.0.5 + + html-escaper@2.0.2: {} + + http-signature@1.2.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.18.0 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + ignore@4.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@2.0.0: + dependencies: + pkg-dir: 3.0.0 + resolve-cwd: 2.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + inquirer@7.3.3: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.18.1 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + is-accessor-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-buffer@1.1.6: {} + + is-callable@1.2.7: {} + + is-ci@2.0.0: + dependencies: + ci-info: 2.0.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-descriptor@1.0.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-descriptor@0.1.7: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-descriptor@1.0.3: + dependencies: + is-accessor-descriptor: 1.0.1 + is-data-descriptor: 1.0.1 + + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-fullwidth-code-point@2.0.0: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-generator-function@1.1.2: + dependencies: + call-bound: 1.0.4 + generator-function: 2.0.1 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@3.0.0: + dependencies: + kind-of: 3.2.2 + + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-stream@1.1.0: {} + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.20 + + is-typedarray@1.0.0: {} + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-windows@1.0.2: {} + + is-wsl@1.1.0: {} + + isarray@1.0.0: {} + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isobject@2.1.0: + dependencies: + isarray: 1.0.0 + + isobject@3.0.1: {} + + isstream@0.1.2: {} + + istanbul-lib-coverage@2.0.5: {} + + istanbul-lib-instrument@3.3.0: + dependencies: + '@babel/generator': 7.29.1 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + istanbul-lib-coverage: 2.0.5 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@2.0.8: + dependencies: + istanbul-lib-coverage: 2.0.5 + make-dir: 2.1.0 + supports-color: 6.1.0 + + istanbul-lib-source-maps@3.0.6: + dependencies: + debug: 4.4.3 + istanbul-lib-coverage: 2.0.5 + make-dir: 2.1.0 + rimraf: 2.7.1 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + istanbul-reports@2.2.7: + dependencies: + html-escaper: 2.0.2 + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + jest-changed-files@24.9.0: + dependencies: + '@jest/types': 24.9.0 + execa: 1.0.0 + throat: 4.1.0 + + jest-cli@24.9.0: + dependencies: + '@jest/core': 24.9.0 + '@jest/test-result': 24.9.0 + '@jest/types': 24.9.0 + chalk: 2.4.2 + exit: 0.1.2 + import-local: 2.0.0 + is-ci: 2.0.0 + jest-config: 24.9.0 + jest-util: 24.9.0 + jest-validate: 24.9.0 + prompts: 2.4.2 + realpath-native: 1.1.0 + yargs: 13.3.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-config@24.9.0: + dependencies: + '@babel/core': 7.29.0 + '@jest/test-sequencer': 24.9.0 + '@jest/types': 24.9.0 + babel-jest: 24.9.0(@babel/core@7.29.0) + chalk: 2.4.2 + glob: 7.2.3 + jest-environment-jsdom: 24.9.0 + jest-environment-node: 24.9.0 + jest-get-type: 24.9.0 + jest-jasmine2: 24.9.0 + jest-regex-util: 24.9.0 + jest-resolve: 24.9.0 + jest-util: 24.9.0 + jest-validate: 24.9.0 + micromatch: 3.1.10 + pretty-format: 24.9.0 + realpath-native: 1.1.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-diff@24.9.0: + dependencies: + chalk: 2.4.2 + diff-sequences: 24.9.0 + jest-get-type: 24.9.0 + pretty-format: 24.9.0 + + jest-docblock@24.9.0: + dependencies: + detect-newline: 2.1.0 + + jest-each@24.9.0: + dependencies: + '@jest/types': 24.9.0 + chalk: 2.4.2 + jest-get-type: 24.9.0 + jest-util: 24.9.0 + pretty-format: 24.9.0 + transitivePeerDependencies: + - supports-color + + jest-environment-jsdom@24.9.0: + dependencies: + '@jest/environment': 24.9.0 + '@jest/fake-timers': 24.9.0 + '@jest/types': 24.9.0 + jest-mock: 24.9.0 + jest-util: 24.9.0 + jsdom: 11.12.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-environment-node@24.9.0: + dependencies: + '@jest/environment': 24.9.0 + '@jest/fake-timers': 24.9.0 + '@jest/types': 24.9.0 + jest-mock: 24.9.0 + jest-util: 24.9.0 + transitivePeerDependencies: + - supports-color + + jest-get-type@24.9.0: {} + + jest-haste-map@24.9.0: + dependencies: + '@jest/types': 24.9.0 + anymatch: 2.0.0 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-serializer: 24.9.0 + jest-util: 24.9.0 + jest-worker: 24.9.0 + micromatch: 3.1.10 + sane: 4.1.0 + walker: 1.0.8 + optionalDependencies: + fsevents: 1.2.13 + transitivePeerDependencies: + - supports-color + + jest-jasmine2@24.9.0: + dependencies: + '@babel/traverse': 7.29.0 + '@jest/environment': 24.9.0 + '@jest/test-result': 24.9.0 + '@jest/types': 24.9.0 + chalk: 2.4.2 + co: 4.6.0 + expect: 24.9.0 + is-generator-fn: 2.1.0 + jest-each: 24.9.0 + jest-matcher-utils: 24.9.0 + jest-message-util: 24.9.0 + jest-runtime: 24.9.0 + jest-snapshot: 24.9.0 + jest-util: 24.9.0 + pretty-format: 24.9.0 + throat: 4.1.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-leak-detector@24.9.0: + dependencies: + jest-get-type: 24.9.0 + pretty-format: 24.9.0 + + jest-matcher-utils@24.9.0: + dependencies: + chalk: 2.4.2 + jest-diff: 24.9.0 + jest-get-type: 24.9.0 + pretty-format: 24.9.0 + + jest-message-util@24.9.0: + dependencies: + '@babel/code-frame': 7.29.0 + '@jest/test-result': 24.9.0 + '@jest/types': 24.9.0 + '@types/stack-utils': 1.0.1 + chalk: 2.4.2 + micromatch: 3.1.10 + slash: 2.0.0 + stack-utils: 1.0.5 + transitivePeerDependencies: + - supports-color + + jest-mock@24.9.0: + dependencies: + '@jest/types': 24.9.0 + + jest-pnp-resolver@1.2.3(jest-resolve@24.9.0): + optionalDependencies: + jest-resolve: 24.9.0 + + jest-regex-util@24.9.0: {} + + jest-resolve-dependencies@24.9.0: + dependencies: + '@jest/types': 24.9.0 + jest-regex-util: 24.9.0 + jest-snapshot: 24.9.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@24.9.0: + dependencies: + '@jest/types': 24.9.0 + browser-resolve: 1.11.3 + chalk: 2.4.2 + jest-pnp-resolver: 1.2.3(jest-resolve@24.9.0) + realpath-native: 1.1.0 + + jest-runner@24.9.0: + dependencies: + '@jest/console': 24.9.0 + '@jest/environment': 24.9.0 + '@jest/test-result': 24.9.0 + '@jest/types': 24.9.0 + chalk: 2.4.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 24.9.0 + jest-docblock: 24.9.0 + jest-haste-map: 24.9.0 + jest-jasmine2: 24.9.0 + jest-leak-detector: 24.9.0 + jest-message-util: 24.9.0 + jest-resolve: 24.9.0 + jest-runtime: 24.9.0 + jest-util: 24.9.0 + jest-worker: 24.9.0 + source-map-support: 0.5.21 + throat: 4.1.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-runtime@24.9.0: + dependencies: + '@jest/console': 24.9.0 + '@jest/environment': 24.9.0 + '@jest/source-map': 24.9.0 + '@jest/transform': 24.9.0 + '@jest/types': 24.9.0 + '@types/yargs': 13.0.12 + chalk: 2.4.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-config: 24.9.0 + jest-haste-map: 24.9.0 + jest-message-util: 24.9.0 + jest-mock: 24.9.0 + jest-regex-util: 24.9.0 + jest-resolve: 24.9.0 + jest-snapshot: 24.9.0 + jest-util: 24.9.0 + jest-validate: 24.9.0 + realpath-native: 1.1.0 + slash: 2.0.0 + strip-bom: 3.0.0 + yargs: 13.3.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jest-serializer@24.9.0: {} + + jest-snapshot@24.9.0: + dependencies: + '@babel/types': 7.29.0 + '@jest/types': 24.9.0 + chalk: 2.4.2 + expect: 24.9.0 + jest-diff: 24.9.0 + jest-get-type: 24.9.0 + jest-matcher-utils: 24.9.0 + jest-message-util: 24.9.0 + jest-resolve: 24.9.0 + mkdirp: 0.5.6 + natural-compare: 1.4.0 + pretty-format: 24.9.0 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + jest-util@24.9.0: + dependencies: + '@jest/console': 24.9.0 + '@jest/fake-timers': 24.9.0 + '@jest/source-map': 24.9.0 + '@jest/test-result': 24.9.0 + '@jest/types': 24.9.0 + callsites: 3.1.0 + chalk: 2.4.2 + graceful-fs: 4.2.11 + is-ci: 2.0.0 + mkdirp: 0.5.6 + slash: 2.0.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + jest-validate@24.9.0: + dependencies: + '@jest/types': 24.9.0 + camelcase: 5.3.1 + chalk: 2.4.2 + jest-get-type: 24.9.0 + leven: 3.1.0 + pretty-format: 24.9.0 + + jest-watcher@24.9.0: + dependencies: + '@jest/test-result': 24.9.0 + '@jest/types': 24.9.0 + '@types/yargs': 13.0.12 + ansi-escapes: 3.2.0 + chalk: 2.4.2 + jest-util: 24.9.0 + string-length: 2.0.0 + transitivePeerDependencies: + - supports-color + + jest-worker@24.9.0: + dependencies: + merge-stream: 2.0.0 + supports-color: 6.1.0 + + jest@24.9.0: + dependencies: + import-local: 2.0.0 + jest-cli: 24.9.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + jsbn@0.1.1: {} + + jscodeshift@0.11.0(@babel/preset-env@7.29.2(@babel/core@7.29.0)): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.29.0) + '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.29.0) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) + '@babel/preset-env': 7.29.2(@babel/core@7.29.0) + '@babel/preset-flow': 7.27.1(@babel/core@7.29.0) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@babel/register': 7.28.6(@babel/core@7.29.0) + babel-core: 7.0.0-bridge.0(@babel/core@7.29.0) + colors: 1.4.0 + flow-parser: 0.309.0 + graceful-fs: 4.2.11 + micromatch: 3.1.10 + neo-async: 2.6.2 + node-dir: 0.1.17 + recast: 0.20.5 + temp: 0.8.4 + write-file-atomic: 2.4.3 + transitivePeerDependencies: + - supports-color + + jsdom@11.12.0: + dependencies: + abab: 2.0.6 + acorn: 5.7.4 + acorn-globals: 4.3.4 + array-equal: 1.0.2 + cssom: 0.3.8 + cssstyle: 1.4.0 + data-urls: 1.1.0 + domexception: 1.0.1 + escodegen: 1.14.3 + html-encoding-sniffer: 1.0.2 + left-pad: 1.3.0 + nwsapi: 2.2.23 + parse5: 4.0.0 + pn: 1.1.0 + request: 2.88.2 + request-promise-native: 1.0.9(request@2.88.2) + sax: 1.6.0 + symbol-tree: 3.2.4 + tough-cookie: 2.5.0 + w3c-hr-time: 1.0.2 + webidl-conversions: 4.0.2 + whatwg-encoding: 1.0.5 + whatwg-mimetype: 2.3.0 + whatwg-url: 6.5.0 + ws: 5.2.4 + xml-name-validator: 3.0.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + jsesc@3.1.0: {} + + json-parse-better-errors@1.0.2: {} + + json-schema-traverse@0.4.1: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-safe@5.0.1: {} + + json5@2.2.3: {} + + jsprim@1.4.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + kind-of@3.2.2: + dependencies: + is-buffer: 1.1.6 + + kind-of@4.0.0: + dependencies: + is-buffer: 1.1.6 + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + left-pad@1.3.0: {} + + leven@3.1.0: {} + + levn@0.3.0: + dependencies: + prelude-ls: 1.1.2 + type-check: 0.3.2 + + load-json-file@4.0.0: + dependencies: + graceful-fs: 4.2.11 + parse-json: 4.0.0 + pify: 3.0.0 + strip-bom: 3.0.0 + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + lodash.debounce@4.0.8: {} + + lodash.memoize@4.1.2: {} + + lodash.sortby@4.7.0: {} + + lodash@4.18.1: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + make-dir@2.1.0: + dependencies: + pify: 4.0.1 + semver: 5.7.2 + + make-error@1.3.6: {} + + makeerror@1.0.12: + dependencies: + tmpl: 1.0.5 + + map-cache@0.2.2: {} + + map-visit@1.0.0: + dependencies: + object-visit: 1.0.1 + + math-intrinsics@1.1.0: {} + + merge-stream@2.0.0: {} + + micromatch@3.1.10: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + braces: 2.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + extglob: 2.0.4 + fragment-cache: 0.2.1 + kind-of: 6.0.3 + nanomatch: 1.2.13 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minimist@1.2.8: {} + + mixin-deep@1.3.2: + dependencies: + for-in: 1.0.2 + is-extendable: 1.0.1 + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + ms@2.0.0: {} + + ms@2.1.3: {} + + mute-stream@0.0.8: {} + + nan@2.26.2: + optional: true + + nanomatch@1.2.13: + dependencies: + arr-diff: 4.0.0 + array-unique: 0.3.2 + define-property: 2.0.2 + extend-shallow: 3.0.2 + fragment-cache: 0.2.1 + is-windows: 1.0.2 + kind-of: 6.0.3 + object.pick: 1.3.0 + regex-not: 1.0.2 + snapdragon: 0.8.2 + to-regex: 3.0.2 + transitivePeerDependencies: + - supports-color + + natural-compare@1.4.0: {} + + neo-async@2.6.2: {} + + nice-try@1.0.5: {} + + node-dir@0.1.17: + dependencies: + minimatch: 3.1.5 + + node-exports-info@1.6.0: + dependencies: + array.prototype.flatmap: 1.3.3 + es-errors: 1.3.0 + object.entries: 1.1.9 + semver: 6.3.1 + + node-int64@0.4.0: {} + + node-notifier@5.4.5: + dependencies: + growly: 1.3.0 + is-wsl: 1.1.0 + semver: 5.7.2 + shellwords: 0.1.1 + which: 1.3.1 + + node-releases@2.0.37: {} + + normalize-package-data@2.5.0: + dependencies: + hosted-git-info: 2.8.9 + resolve: 1.22.12 + semver: 5.7.2 + validate-npm-package-license: 3.0.4 + + normalize-path@2.1.1: + dependencies: + remove-trailing-separator: 1.1.0 + + npm-run-path@2.0.2: + dependencies: + path-key: 2.0.1 + + nwsapi@2.2.23: {} + + oauth-sign@0.9.0: {} + + object-assign@4.1.1: {} + + object-copy@0.1.0: + dependencies: + copy-descriptor: 0.1.1 + define-property: 0.2.5 + kind-of: 3.2.2 + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object-visit@1.0.1: + dependencies: + isobject: 3.0.1 + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + + object.getownpropertydescriptors@2.1.9: + dependencies: + array.prototype.reduce: 1.0.8 + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + gopd: 1.2.0 + safe-array-concat: 1.1.3 + + object.pick@1.3.0: + dependencies: + isobject: 3.0.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.8.3: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.3.0 + prelude-ls: 1.1.2 + type-check: 0.3.2 + word-wrap: 1.2.5 + + os-tmpdir@1.0.2: {} + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-each-series@1.0.0: + dependencies: + p-reduce: 1.0.0 + + p-finally@1.0.0: {} + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-reduce@1.0.0: {} + + p-try@2.2.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@4.0.0: + dependencies: + error-ex: 1.3.4 + json-parse-better-errors: 1.0.2 + + parse5@4.0.0: {} + + pascalcase@0.1.1: {} + + path-exists@3.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@2.0.1: {} + + path-parse@1.0.7: {} + + path-type@3.0.0: + dependencies: + pify: 3.0.0 + + performance-now@2.1.0: {} + + picocolors@1.1.1: {} + + pify@3.0.0: {} + + pify@4.0.1: {} + + pirates@4.0.7: {} + + pkg-dir@3.0.0: + dependencies: + find-up: 3.0.0 + + pn@1.1.0: {} + + posix-character-classes@0.1.1: {} + + possible-typed-array-names@1.1.0: {} + + prelude-ls@1.1.2: {} + + pretty-format@24.9.0: + dependencies: + '@jest/types': 24.9.0 + ansi-regex: 4.1.1 + ansi-styles: 3.2.1 + react-is: 16.13.1 + + progress@2.0.3: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + psl@1.15.0: + dependencies: + punycode: 2.3.1 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.5.5: {} + + react-is@16.13.1: {} + + read-pkg-up@4.0.0: + dependencies: + find-up: 3.0.0 + read-pkg: 3.0.0 + + read-pkg@3.0.0: + dependencies: + load-json-file: 4.0.0 + normalize-package-data: 2.5.0 + path-type: 3.0.0 + + realpath-native@1.1.0: + dependencies: + util.promisify: 1.1.3 + + recast@0.20.5: + dependencies: + ast-types: 0.14.2 + esprima: 4.0.1 + source-map: 0.6.1 + tslib: 2.8.1 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regenerate-unicode-properties@10.2.2: + dependencies: + regenerate: 1.4.2 + + regenerate@1.4.2: {} + + regex-not@1.0.2: + dependencies: + extend-shallow: 3.0.2 + safe-regex: 1.1.0 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + regexpp@2.0.1: {} + + regexpu-core@6.4.0: + dependencies: + regenerate: 1.4.2 + regenerate-unicode-properties: 10.2.2 + regjsgen: 0.8.0 + regjsparser: 0.13.1 + unicode-match-property-ecmascript: 2.0.0 + unicode-match-property-value-ecmascript: 2.2.1 + + regjsgen@0.8.0: {} + + regjsparser@0.13.1: + dependencies: + jsesc: 3.1.0 + + remove-trailing-separator@1.1.0: {} + + repeat-element@1.1.4: {} + + repeat-string@1.6.1: {} + + request-promise-core@1.1.4(request@2.88.2): + dependencies: + lodash: 4.18.1 + request: 2.88.2 + + request-promise-native@1.0.9(request@2.88.2): + dependencies: + request: 2.88.2 + request-promise-core: 1.1.4(request@2.88.2) + stealthy-require: 1.1.1 + tough-cookie: 2.5.0 + + request@2.88.2: + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.5 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + + require-directory@2.1.1: {} + + require-main-filename@2.0.0: {} + + resolve-cwd@2.0.0: + dependencies: + resolve-from: 3.0.0 + + resolve-from@3.0.0: {} + + resolve-from@4.0.0: {} + + resolve-url@0.2.1: {} + + resolve@1.1.7: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.6: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + node-exports-info: 1.6.0 + object-keys: 1.1.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + ret@0.1.15: {} + + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + + rsvp@4.8.5: {} + + run-async@2.4.1: {} + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-regex@1.1.0: + dependencies: + ret: 0.1.15 + + safer-buffer@2.1.2: {} + + sane@4.1.0: + dependencies: + '@cnakazawa/watch': 1.0.4 + anymatch: 2.0.0 + capture-exit: 2.0.0 + exec-sh: 0.3.6 + execa: 1.0.0 + fb-watchman: 2.0.2 + micromatch: 3.1.10 + minimist: 1.2.8 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + sax@1.6.0: {} + + semver@5.7.2: {} + + semver@6.3.1: {} + + set-blocking@2.0.0: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + + shallow-clone@3.0.1: + dependencies: + kind-of: 6.0.3 + + shebang-command@1.2.0: + dependencies: + shebang-regex: 1.0.0 + + shebang-regex@1.0.0: {} + + shellwords@0.1.1: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + sisteransi@1.0.5: {} + + slash@2.0.0: {} + + slice-ansi@2.1.0: + dependencies: + ansi-styles: 3.2.1 + astral-regex: 1.0.0 + is-fullwidth-code-point: 2.0.0 + + snapdragon-node@2.1.1: + dependencies: + define-property: 1.0.0 + isobject: 3.0.1 + snapdragon-util: 3.0.1 + + snapdragon-util@3.0.1: + dependencies: + kind-of: 3.2.2 + + snapdragon@0.8.2: + dependencies: + base: 0.11.2 + debug: 2.6.9 + define-property: 0.2.5 + extend-shallow: 2.0.1 + map-cache: 0.2.2 + source-map: 0.5.7 + source-map-resolve: 0.5.3 + use: 3.1.1 + transitivePeerDependencies: + - supports-color + + source-map-resolve@0.5.3: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.2 + resolve-url: 0.2.1 + source-map-url: 0.4.1 + urix: 0.1.0 + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map-url@0.4.1: {} + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + spdx-correct@3.2.0: + dependencies: + spdx-expression-parse: 3.0.1 + spdx-license-ids: 3.0.23 + + spdx-exceptions@2.5.0: {} + + spdx-expression-parse@3.0.1: + dependencies: + spdx-exceptions: 2.5.0 + spdx-license-ids: 3.0.23 + + spdx-license-ids@3.0.23: {} + + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + + sprintf-js@1.0.3: {} + + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + stack-utils@1.0.5: + dependencies: + escape-string-regexp: 2.0.0 + + static-extend@0.1.2: + dependencies: + define-property: 0.2.5 + object-copy: 0.1.0 + + stealthy-require@1.1.1: {} + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string-length@2.0.0: + dependencies: + astral-regex: 1.0.0 + strip-ansi: 4.0.0 + + string-width@3.1.0: + dependencies: + emoji-regex: 7.0.3 + is-fullwidth-code-point: 2.0.0 + strip-ansi: 5.2.0 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.2 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.2 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.9 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-ansi@4.0.0: + dependencies: + ansi-regex: 3.0.1 + + strip-ansi@5.2.0: + dependencies: + ansi-regex: 4.1.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + strip-eof@1.0.0: {} + + strip-json-comments@3.1.1: {} + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@6.1.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + table@5.4.6: + dependencies: + ajv: 6.14.0 + lodash: 4.18.1 + slice-ansi: 2.1.0 + string-width: 3.1.0 + + temp@0.8.4: + dependencies: + rimraf: 2.6.3 + + test-exclude@5.2.3: + dependencies: + glob: 7.2.3 + minimatch: 3.1.5 + read-pkg-up: 4.0.0 + require-main-filename: 2.0.0 + + text-table@0.2.0: {} + + throat@4.1.0: {} + + through@2.3.8: {} + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + tmpl@1.0.5: {} + + to-object-path@0.3.0: + dependencies: + kind-of: 3.2.2 + + to-regex-range@2.1.1: + dependencies: + is-number: 3.0.0 + repeat-string: 1.6.1 + + to-regex@3.0.2: + dependencies: + define-property: 2.0.2 + extend-shallow: 3.0.2 + regex-not: 1.0.2 + safe-regex: 1.1.0 + + tough-cookie@2.5.0: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + + ts-jest@24.3.0(jest@24.9.0): + dependencies: + bs-logger: 0.2.6 + buffer-from: 1.1.2 + fast-json-stable-stringify: 2.1.0 + jest: 24.9.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + mkdirp: 0.5.6 + resolve: 1.22.12 + semver: 5.7.2 + yargs-parser: 10.1.0 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tweetnacl@0.14.5: {} + + type-check@0.3.2: + dependencies: + prelude-ls: 1.1.2 + + type-fest@0.21.3: {} + + type-fest@0.8.1: {} + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.9 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript@4.8.4: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + unicode-canonical-property-names-ecmascript@2.0.1: {} + + unicode-match-property-ecmascript@2.0.0: + dependencies: + unicode-canonical-property-names-ecmascript: 2.0.1 + unicode-property-aliases-ecmascript: 2.2.0 + + unicode-match-property-value-ecmascript@2.2.1: {} + + unicode-property-aliases-ecmascript@2.2.0: {} + + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + + unset-value@1.0.0: + dependencies: + has-value: 0.3.1 + isobject: 3.0.1 + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + urix@0.1.0: {} + + use@3.1.1: {} + + util.promisify@1.1.3: + dependencies: + call-bind: 1.0.9 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + for-each: 0.3.5 + get-intrinsic: 1.3.0 + has-proto: 1.2.0 + has-symbols: 1.1.0 + object.getownpropertydescriptors: 2.1.9 + safe-array-concat: 1.1.3 + + uuid@3.4.0: {} + + v8-compile-cache@2.4.0: {} + + validate-npm-package-license@3.0.4: + dependencies: + spdx-correct: 3.2.0 + spdx-expression-parse: 3.0.1 + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + + w3c-hr-time@1.0.2: + dependencies: + browser-process-hrtime: 1.0.0 + + walker@1.0.8: + dependencies: + makeerror: 1.0.12 + + webidl-conversions@4.0.2: {} + + whatwg-encoding@1.0.5: + dependencies: + iconv-lite: 0.4.24 + + whatwg-mimetype@2.3.0: {} + + whatwg-url@6.5.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.2 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-module@2.0.1: {} + + which-typed-array@1.1.20: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.9 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@1.3.1: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@5.1.0: + dependencies: + ansi-styles: 3.2.1 + string-width: 3.1.0 + strip-ansi: 5.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@2.4.1: + dependencies: + graceful-fs: 4.2.11 + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + write-file-atomic@2.4.3: + dependencies: + graceful-fs: 4.2.11 + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + write@1.0.3: + dependencies: + mkdirp: 0.5.6 + + ws@5.2.4: + dependencies: + async-limiter: 1.0.1 + + xml-name-validator@3.0.0: {} + + y18n@4.0.3: {} + + yallist@3.1.1: {} + + yargs-parser@10.1.0: + dependencies: + camelcase: 4.1.0 + + yargs-parser@13.1.2: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 + + yargs@13.3.2: + dependencies: + cliui: 5.0.0 + find-up: 3.0.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 + string-width: 3.1.0 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 13.1.2 diff --git a/codemods/legacy/transforms/React-DOM-to-react-dom-factories.js b/codemods/legacy/transforms/React-DOM-to-react-dom-factories.js new file mode 100644 index 0000000..55a8b4a --- /dev/null +++ b/codemods/legacy/transforms/React-DOM-to-react-dom-factories.js @@ -0,0 +1,177 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +module.exports = function(file, api, options) { + const j = api.jscodeshift; + const printOptions = options.printOptions || { quote: 'single' }; + const root = j(file.source); + + let hasModifications; + + const DOMModuleName = 'DOM'; + + /** + * Replaces 'DOM' with 'createElement' in places where we grab 'DOM' out of + * 'React' with destructuring. + * Note that this only picks up 'DOM' when required from React or + * require('react') + */ + const replaceDestructuredDOMStatement = (j, root) => { + let hasModifications = false; + //--------- + // First update import statments. eg: + // import { + // DOM, + // foo, + // } from 'react'; + root + .find(j.ImportDeclaration) + .filter( + path => + path.node.specifiers.filter( + specifier => + specifier.imported && specifier.imported.name === DOMModuleName + ).length > 0 && path.node.source.value === 'react' + ) + .forEach(path => { + hasModifications = true; + + // Replace the DOM key with 'createElement' + path.node.specifiers = path.node.specifiers.map(specifier => { + if (specifier.imported && specifier.imported.name === DOMModuleName) { + return j.importSpecifier(j.identifier('createElement')); + } else { + return specifier; + } + }); + }); + + //--------- + // Next update require statments. + // This matches both + // const { + // Component, + // DOM, + // } = React; + // and + // const { + // Component, + // DOM, + // } = require('react'); + root + .find(j.ObjectPattern) + .filter( + path => + path.parent.node.init && + // matches '} = React;' + (path.parent.node.init.name === 'React' || + // matches "} = require('react');" + (path.parent.node.init.type === 'CallExpression' && + path.parent.node.init.callee.name === 'require' && + path.parent.node.init.arguments[0].value === 'react')) && + path.node.properties.some(property => { + return property.key.name === DOMModuleName; + }) + ) + .forEach(path => { + hasModifications = true; + + // Replace the DOM key with 'createElement' + path.node.properties = path.node.properties.map(property => { + if (property.key.name === DOMModuleName) { + return j.identifier('createElement'); + } else { + return property; + } + }); + }); + return hasModifications; + }; + + hasModifications = + replaceDestructuredDOMStatement(j, root) || hasModifications; + + if (hasModifications) { + // if we 'hasModifications' then we found and replaced a reference to + // '{DOM} = React;' or '{DOM} = require('react');' + // In this case we need to update 'DOM.' syntax + + /** + * Update cases where DOM.div is being called + * eg 'foo = DOM.div('a'...' + * replace with 'foo = createElement('div', 'a'...' + */ + function replaceDOMReferences(j, root) { + let hasModifications = false; + + const isDOMIdentifier = path => + path.node.name === DOMModuleName && + path.parent.parent.node.type === 'CallExpression'; + + root + .find(j.Identifier) + .filter(isDOMIdentifier) + .forEach(path => { + hasModifications = true; + + const DOMargs = path.parent.parent.node.arguments; + const DOMFactoryPath = path.parent.node.property; + const DOMFactoryType = DOMFactoryPath.name; + + // DOM.div(... -> createElement(... + j(path.parent).replaceWith(j.identifier('createElement')); + // createElement(... -> createElement('div', ... + DOMargs.unshift(j.literal(DOMFactoryType)); + }); + + return hasModifications; + } + + hasModifications = replaceDOMReferences(j, root) || hasModifications; + } + + /** + * Update React.DOM references + * eg 'foo = React.DOM.div('a'...' + * replace with 'foo = React.createElement('div', 'a'...' + */ + function replaceReactDOMReferences(j, root) { + let hasModifications = false; + + // matches 'React.DOM' + const isReactDOMIdentifier = path => + path.node.name === DOMModuleName && + (path.parent.node.type === 'MemberExpression' && + path.parent.node.object.name === 'React'); + + root + .find(j.Identifier) + .filter(isReactDOMIdentifier) + .forEach(path => { + hasModifications = true; + const DOMargs = path.parent.parent.parent.node.arguments; + const DOMFactoryPath = path.parent.parent.node.property; + const DOMFactoryType = DOMFactoryPath.name; + + // React.DOM.div(... -> React.DOM.createElement(... + path.parent.parent.node.property = j.identifier('createElement'); + // React.DOM.createElement(... -> React.createElement(... + j(path.parent).replaceWith(j.identifier('React')); + // React.createElement(... -> React.createElement('div'... + DOMargs.unshift(j.literal(DOMFactoryType)); + }); + + return hasModifications; + } + + hasModifications = replaceReactDOMReferences(j, root) || hasModifications; + + return hasModifications ? root.toSource(printOptions) : null; +}; diff --git a/codemods/legacy/transforms/ReactNative-View-propTypes.js b/codemods/legacy/transforms/ReactNative-View-propTypes.js new file mode 100644 index 0000000..70ec81a --- /dev/null +++ b/codemods/legacy/transforms/ReactNative-View-propTypes.js @@ -0,0 +1,218 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const isReactNativeImport = path => + path.parent.node.source.value === 'react-native'; + +const isReactNativeRequire = path => + path.node.arguments.some(argument => argument.value === 'react-native'); + +const isRootViewReference = path => + path.node.name === 'View' && + (path.parent.node.type !== 'MemberExpression' || + path.parent.node.object === path.node) && + (path.node.type !== 'JSXIdentifier' || + path.parent.node.type === 'JSXOpeningElement') && + (path.parent.node.type !== 'ImportSpecifier' || + path.parent.node.imported === path.node) && + (path.parent.node.type !== 'Property' || path.parent.node.key === path.node); + +const isViewImport = path => + path.node.specifiers.some( + specifier => + (specifier.imported && specifier.imported.name === 'View') || + (specifier.local && specifier.local.name === 'View') + ); + +const isViewRequire = path => + path.node.callee.type === 'Identifier' && + path.parent.node.type === 'VariableDeclarator' && + ((path.parent.node.id.type === 'Identifier' && + path.parent.node.id.name === 'View') || + (path.parent.node.id.type === 'ObjectPattern' && + path.parent.node.id.properties.some( + property => property.value.name === 'View' + ))); + +const isViewPropTypes = path => + path.node.name === 'propTypes' && + path.parent.node.type === 'MemberExpression' && + path.parent.value.object.name === 'View'; + +// Note that this codemod may introduce an unnecessary newline before certain types of imports +// This is not a problem with the codemod but with recast +// See https://github.com/facebook/jscodeshift/issues/185 +// See https://github.com/benjamn/recast/issues/371 +module.exports = function(file, api, options) { + const j = api.jscodeshift; + + const printOptions = options.printOptions || { quote: 'single' }; + let root = j(file.source); + + let numMatchedPaths = 0; + + // Search for all View references + const viewReferenceCount = root.find(j.Identifier).filter(isRootViewReference) + .length; + + // Replace View.propTypes with ViewPropTypes + root + .find(j.Identifier) + .filter(isViewPropTypes) + .forEach(path => { + numMatchedPaths++; + + j(path.parent).replaceWith(j.identifier('ViewPropTypes')); + }); + + // Add ViewPropTypes import/require() + if (numMatchedPaths > 0) { + const fileUsesImports = + root.find(j.CallExpression, { callee: { name: 'require' } }).length === 0; + + // Determine which kind of import/require() we should create based on file contents + let useHasteModules = false; + if (fileUsesImports) { + useHasteModules = + root.find(j.ImportSpecifier).filter(isReactNativeImport).length === 0; + } else { + useHasteModules = + root + .find(j.CallExpression, { callee: { name: 'require' } }) + .filter(isReactNativeRequire).length === 0; + } + + // Create a require statement or an import, based on file convention + let importOrRequireStatement; + if (fileUsesImports) { + const identifier = j.identifier('ViewPropTypes'); + const variable = + useHasteModules === true + ? j.importDefaultSpecifier(identifier) + : j.importSpecifier(identifier); + const source = + useHasteModules === true ? 'ViewPropTypes' : 'react-native'; + + importOrRequireStatement = j.importDeclaration( + [variable], + j.literal(source) + ); + } else { + if (useHasteModules === true) { + importOrRequireStatement = j.template.statement` + const ViewPropTypes = require('ViewPropTypes'); + `; + } else { + importOrRequireStatement = j.template.statement` + const { ViewPropTypes } = require('react-native'); + `; + } + } + + // If the only View reference left is the import/require(), replace it + // Else insert our new import/require() after it + // Add one to avoid counting the import/require() statement itself + const replaceExistingImportOrRequireStatement = + viewReferenceCount <= numMatchedPaths + 1; + + if (fileUsesImports) { + root + .find(j.ImportDeclaration) + .filter(isViewImport) + .forEach(path => { + // Differentiate between destructured and default import + if (path.node.specifiers.length > 1) { + if (useHasteModules) { + // Insert after before removing to avoid an error + j(path).insertAfter(importOrRequireStatement); + + if (replaceExistingImportOrRequireStatement) { + // If this is the last reference to a destructured import, remove it + // We can't replace in this case b'c the target/source is different + path.node.specifiers = path.node.specifiers.filter( + specifier => specifier.local.name !== 'View' + ); + } + } else { + if (replaceExistingImportOrRequireStatement) { + const viewImport = path.node.specifiers.find( + specifier => specifier.local.name === 'View' + ); + + viewImport.local.name = 'ViewPropTypes'; + } else { + path.node.specifiers.push( + j.importSpecifier( + j.identifier('ViewPropTypes'), + j.identifier('ViewPropTypes') + ) + ); + } + } + } else { + if (replaceExistingImportOrRequireStatement) { + j(path).replaceWith(importOrRequireStatement); + } else { + j(path).insertAfter(importOrRequireStatement); + } + } + }); + } else { + root + .find(j.CallExpression, { callee: { name: 'require' } }) + .filter(isViewRequire) + .forEach(path => { + // Differentiate between destructured and default require() + if (path.parent.node.id.type === 'ObjectPattern') { + if (useHasteModules) { + // Insert after before removing to avoid an error + j(path.parent.parent).insertAfter(importOrRequireStatement); + + if (replaceExistingImportOrRequireStatement) { + // If this is the last reference to a destructured import, remove it + // We can't replace in this case b'c the target/source is different + const variableDeclarator = + path.parent.parent.value.declarations[0]; + variableDeclarator.id.properties = variableDeclarator.id.properties.filter( + property => property.value.name !== 'View' + ); + } + } else { + const objectPattern = path.parent; + if (replaceExistingImportOrRequireStatement) { + const property = objectPattern.node.id.properties.find( + property => property.value.name === 'View' + ); + + property.key.name = 'ViewPropTypes'; + } else { + const property = j.property( + 'init', + j.identifier('ViewPropTypes'), + j.identifier('ViewPropTypes') + ); + property.shorthand = true; + + objectPattern.node.id.properties.push(property); + } + } + } else { + if (replaceExistingImportOrRequireStatement) { + j(path.parent.parent).replaceWith(importOrRequireStatement); + } else { + j(path.parent.parent).insertAfter(importOrRequireStatement); + } + } + }); + } + } + + return numMatchedPaths > 0 ? root.toSource(printOptions) : null; +}; diff --git a/codemods/legacy/transforms/__testfixtures__/.eslintrc b/codemods/legacy/transforms/__testfixtures__/.eslintrc new file mode 100644 index 0000000..4a2e77c --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/.eslintrc @@ -0,0 +1,5 @@ +--- +rules: + no-undef: 0 + no-unused-vars: 0 + no-redeclare: 0 diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/input.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.input.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/input.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.input.js diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/expected.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.output.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-basic-case.output.js diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/input.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.input.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/input.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.input.js diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/expected.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.output.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-import.output.js diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/input.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.input.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/input.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.input.js diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/expected.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.output.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require-part-two.output.js diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/input.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.input.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/input.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.input.js diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/expected.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.output.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-deconstructed-require.output.js diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/expected.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import-dom-from-other-libraries.input.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import-dom-from-other-libraries.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import-dom-from-other-libraries.output.js b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import-dom-from-other-libraries.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/expected.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.input.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.output.js b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-import.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/expected.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-local-dom-from-other-libraries.input.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-local-dom-from-other-libraries.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-local-dom-from-other-libraries.output.js b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-local-dom-from-other-libraries.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/expected.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require-dom-from-other-libraries.input.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require-dom-from-other-libraries.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require-dom-from-other-libraries.output.js b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require-dom-from-other-libraries.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/expected.tsx b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.input.js similarity index 100% rename from codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.output.js b/codemods/legacy/transforms/__testfixtures__/React-DOM-to-react-dom-factories/react-dom-no-change-require.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/react-native-view-prop-types/tests/default-import-multi-reference/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-multi-reference.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/default-import-multi-reference/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-multi-reference.input.js diff --git a/codemods/react-native-view-prop-types/tests/default-import-multi-reference/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-multi-reference.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/default-import-multi-reference/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-multi-reference.output.js diff --git a/codemods/react-native-view-prop-types/tests/default-import-only-reference/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-only-reference.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/default-import-only-reference/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-only-reference.input.js diff --git a/codemods/react-native-view-prop-types/tests/default-import-only-reference/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-only-reference.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/default-import-only-reference/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-only-reference.output.js diff --git a/codemods/react-native-view-prop-types/tests/default-require-multi-reference/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-multi-reference.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/default-require-multi-reference/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-multi-reference.input.js diff --git a/codemods/react-native-view-prop-types/tests/default-require-multi-reference/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-multi-reference.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/default-require-multi-reference/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-multi-reference.output.js diff --git a/codemods/react-native-view-prop-types/tests/default-require-only-reference/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-only-reference.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/default-require-only-reference/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-only-reference.input.js diff --git a/codemods/react-native-view-prop-types/tests/default-require-only-reference/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-only-reference.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/default-require-only-reference/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/default-require-only-reference.output.js diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-multi-reference.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-multi-reference.input.js diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-multi-reference.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-multi-reference.output.js diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-only-reference.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/destructured-import-only-reference/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-only-reference.input.js diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-only-reference.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/destructured-import-only-reference/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-import-only-reference.output.js diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-multi-reference.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-multi-reference.input.js diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-multi-reference.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-multi-reference.output.js diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-only-reference.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/destructured-require-only-reference/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-only-reference.input.js diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-only-reference.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/destructured-require-only-reference/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/destructured-require-only-reference.output.js diff --git a/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/import-flow-type-with-require.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/import-flow-type-with-require/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/import-flow-type-with-require.input.js diff --git a/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/import-flow-type-with-require.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/import-flow-type-with-require/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/import-flow-type-with-require.output.js diff --git a/codemods/react-native-view-prop-types/tests/multiple-replacements/input.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/multiple-replacements.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/multiple-replacements/input.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/multiple-replacements.input.js diff --git a/codemods/react-native-view-prop-types/tests/multiple-replacements/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/multiple-replacements.output.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/multiple-replacements/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/multiple-replacements.output.js diff --git a/codemods/react-native-view-prop-types/tests/noop-import/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-import.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/noop-import/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-import.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-import.output.js b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-import.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/react-native-view-prop-types/tests/noop-require/expected.tsx b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-require.input.js similarity index 100% rename from codemods/react-native-view-prop-types/tests/noop-require/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-require.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-require.output.js b/codemods/legacy/transforms/__testfixtures__/ReactNative-View-propTypes/noop-require.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-anonymous.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-anonymous.input.js new file mode 100644 index 0000000..22b4ca7 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-anonymous.input.js @@ -0,0 +1,21 @@ +var React = require('react'); + +const wrapper = (x) => x; + +const Foo = wrapper(React.createClass({ + render() { + return
    wow so anonymous
    ; + }, +})); + +module.exports = wrapper(React.createClass({ + render() { + return
    wow so anonymous
    ; + }, +})); + +export default wrapper(React.createClass({ + render() { + return
    wow so anonymous
    ; + }, +})); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-anonymous.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-anonymous.output.js new file mode 100644 index 0000000..c90ecbc --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-anonymous.output.js @@ -0,0 +1,21 @@ +var React = require('react'); + +const wrapper = (x) => x; + +const Foo = wrapper(class extends React.Component { + render() { + return
    wow so anonymous
    ; + } +}); + +module.exports = wrapper(class extends React.Component { + render() { + return
    wow so anonymous
    ; + } +}); + +export default wrapper(class extends React.Component { + render() { + return
    wow so anonymous
    ; + } +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-anonymous2.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-anonymous2.input.js new file mode 100644 index 0000000..a902474 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-anonymous2.input.js @@ -0,0 +1,50 @@ +/** + * @flow + */ +/* eslint-disable no-use-before-define */ +'use strict'; + +var React = require('React'); + +var CrazyObject = { + foo: { + bar: 123, + }, + method: { + wrapThisGuy: (x) => x, + deep: { + wrapThatGuy: (x) => x, + }, + }, + iDontUnderstand: { + whyYouDoThis: { + butAnyway: { + comp1: React.createClass({ + render() { + return
    ; + }, + }), + comp2: CrazyObject.method.wrapThatGuy(React.createClass({ + render() { + return
    ; + }, + })), + waitWhatArrayForReal: [React.createClass({ + render() { + return
    ; + }, + }), [React.createClass({ + render() { + return

    ; + }, + }), React.createClass({ + render() { + return ; + }, + })]], + }, + }, + }, +}; + +module.exports = WaltUtils; diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-anonymous2.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-anonymous2.output.js new file mode 100644 index 0000000..1b7a8fc --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-anonymous2.output.js @@ -0,0 +1,50 @@ +/** + * @flow + */ +/* eslint-disable no-use-before-define */ +'use strict'; + +var React = require('React'); + +var CrazyObject = { + foo: { + bar: 123, + }, + method: { + wrapThisGuy: (x) => x, + deep: { + wrapThatGuy: (x) => x, + }, + }, + iDontUnderstand: { + whyYouDoThis: { + butAnyway: { + comp1: class extends React.Component { + render() { + return

    ; + } + }, + comp2: CrazyObject.method.wrapThatGuy(class extends React.Component { + render() { + return
    ; + } + }), + waitWhatArrayForReal: [class extends React.Component { + render() { + return
    ; + } + }, [class extends React.Component { + render() { + return

    ; + } + }, class extends React.Component { + render() { + return ; + } + }]], + }, + }, + }, +}; + +module.exports = WaltUtils; diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-create-class-naming.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-create-class-naming.input.js new file mode 100644 index 0000000..5d6ed33 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-create-class-naming.input.js @@ -0,0 +1,12 @@ +// Uses options: +// --create-class-module-name=createReactClass__deprecated +// --create-class-variable-name=createReactClass__deprecated + +const React = require('react'); + +const Component = React.createClass({ + mixins: [{}], + render() { + return

    ; + } +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-create-class-naming.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-create-class-naming.output.js new file mode 100644 index 0000000..85e9385 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-create-class-naming.output.js @@ -0,0 +1,16 @@ +// Uses options: +// --create-class-module-name=createReactClass__deprecated +// --create-class-variable-name=createReactClass__deprecated + +const React = require('react'); + +const createReactClass__deprecated = require('createReactClass__deprecated'); + +const Component = createReactClass__deprecated({ + displayName: 'Component', + mixins: [{}], + + render() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-displayName.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-displayName.input.js new file mode 100644 index 0000000..61aac9c --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-displayName.input.js @@ -0,0 +1,31 @@ +const React = require('React'); + +let A = React.createClass({ + mixins: [], + render() { + return
    ; + }, +}); + +A = React.createClass({ + mixins: [], + render() { + return
    ; + }, +}); + +const obj = { + B: React.createClass({ + mixins: [], + render() { + return
    ; + }, + }), +}; + +export default React.createClass({ + mixins: [], + render() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-displayName.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-displayName.output.js new file mode 100644 index 0000000..1018735 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-displayName.output.js @@ -0,0 +1,41 @@ +const React = require('React'); + +const createReactClass = require('create-react-class'); + +let A = createReactClass({ + displayName: 'A', + mixins: [], + + render() { + return
    ; + }, +}); + +A = createReactClass({ + displayName: 'A', + mixins: [], + + render() { + return
    ; + }, +}); + +const obj = { + B: createReactClass({ + displayName: 'B', + mixins: [], + + render() { + return
    ; + }, + }), +}; + +export default createReactClass({ + displayName: 'class-displayName.input', + mixins: [], + + render() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow1.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow1.input.js new file mode 100644 index 0000000..da855cb --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow1.input.js @@ -0,0 +1,40 @@ +/* @flow */ + +var React = require('react'); + +var Component = React.createClass({ + propTypes: { + optionalArray: React.PropTypes.array, + optionalBool: React.PropTypes.bool, + optionalFunc: React.PropTypes.func, + optionalNumber: React.PropTypes.number, + optionalObject: React.PropTypes.object, + optionalString: React.PropTypes.string, + optionalNode: React.PropTypes.node, + optionalElement: React.PropTypes.element, + optionalMessage: React.PropTypes.instanceOf(Message), + optionalEnum: React.PropTypes.oneOf(['News', 'Photos', 1, true, null, undefined]), + optionalUnion: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + React.PropTypes.instanceOf(Message), + ]), + optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), + optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), + optionalObjectOfRequiredField: React.PropTypes.objectOf(React.PropTypes.number.isRequired), + requiredObjectOfRequiredField: React.PropTypes.objectOf(React.PropTypes.number.isRequired).isRequired, + requiredObjectOfOptionalField: React.PropTypes.objectOf(React.PropTypes.number).isRequired, + optionalObjectWithShape: React.PropTypes.shape({ + color: React.PropTypes.string, + fontSize: React.PropTypes.number.isRequired, + }), + requiredFunc: React.PropTypes.func.isRequired, + requiredAny: React.PropTypes.any.isRequired, + }, + + render: function() { + return ( +
    type safety
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow1.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow1.output.js new file mode 100644 index 0000000..7e50622 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow1.output.js @@ -0,0 +1,65 @@ +/* @flow */ + +var React = require('react'); + +class Component extends React.Component { + props: { + optionalArray?: Array<$FlowFixMe>, + optionalBool?: boolean, + optionalFunc?: Function, + optionalNumber?: number, + optionalObject?: Object, + optionalString?: string, + optionalNode?: $FlowFixMe, + optionalElement?: $FlowFixMe, + optionalMessage?: Message, + optionalEnum?: 'News' | 'Photos' | 1 | true | null | void, + optionalUnion?: string | number | Message, + optionalArrayOf?: Array, + optionalObjectOf?: {[key: string]: number}, + optionalObjectOfRequiredField?: {[key: string]: number}, + requiredObjectOfRequiredField: {[key: string]: number}, + requiredObjectOfOptionalField: {[key: string]: number}, + optionalObjectWithShape?: { + color?: string, + fontSize: number, + }, + requiredFunc: Function, + requiredAny: any, + }; + + static propTypes = { + optionalArray: React.PropTypes.array, + optionalBool: React.PropTypes.bool, + optionalFunc: React.PropTypes.func, + optionalNumber: React.PropTypes.number, + optionalObject: React.PropTypes.object, + optionalString: React.PropTypes.string, + optionalNode: React.PropTypes.node, + optionalElement: React.PropTypes.element, + optionalMessage: React.PropTypes.instanceOf(Message), + optionalEnum: React.PropTypes.oneOf(['News', 'Photos', 1, true, null, undefined]), + optionalUnion: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + React.PropTypes.instanceOf(Message), + ]), + optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), + optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), + optionalObjectOfRequiredField: React.PropTypes.objectOf(React.PropTypes.number.isRequired), + requiredObjectOfRequiredField: React.PropTypes.objectOf(React.PropTypes.number.isRequired).isRequired, + requiredObjectOfOptionalField: React.PropTypes.objectOf(React.PropTypes.number).isRequired, + optionalObjectWithShape: React.PropTypes.shape({ + color: React.PropTypes.string, + fontSize: React.PropTypes.number.isRequired, + }), + requiredFunc: React.PropTypes.func.isRequired, + requiredAny: React.PropTypes.any.isRequired, + }; + + render() { + return ( +
    type safety
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow2.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow2.input.js new file mode 100644 index 0000000..490706c --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow2.input.js @@ -0,0 +1,52 @@ +/* code taken from https://github.com/reactjs/react-router/blob/master/modules/IndexRoute.js */ +/* @flow */ + +import React from 'react' +import warning from './routerWarning' +import invariant from 'invariant' +import { createRouteFromReactElement } from './RouteUtils' +import { component, components, falsy } from './InternalPropTypes' + +const { func } = React.PropTypes + +/** + * An is used to specify its parent's in + * a JSX route config. + */ +const IndexRoute = React.createClass({ + + statics: { + + createRouteFromReactElement(element, parentRoute) { + /* istanbul ignore else: sanity check */ + if (parentRoute) { + parentRoute.indexRoute = createRouteFromReactElement(element) + } else { + warning( + false, + 'An does not make sense at the root of your route config' + ) + } + } + + }, + + propTypes: { + path: falsy, + component, + components, + getComponent: func, + getComponents: func + }, + + /* istanbul ignore next: sanity check */ + render() { + invariant( + false, + ' elements are for router configuration only and should not be rendered' + ) + } + +}) + +export default IndexRoute diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow2.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow2.output.js new file mode 100644 index 0000000..3a6b74d --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow2.output.js @@ -0,0 +1,54 @@ +/* code taken from https://github.com/reactjs/react-router/blob/master/modules/IndexRoute.js */ +/* @flow */ + +import React from 'react' +import warning from './routerWarning' +import invariant from 'invariant' +import { createRouteFromReactElement } from './RouteUtils' +import { component, components, falsy } from './InternalPropTypes' + +const { func } = React.PropTypes + +/** + * An is used to specify its parent's in + * a JSX route config. + */ +class IndexRoute extends React.Component { + props: { + path?: $FlowFixMe, + component?: $FlowFixMe, + components?: $FlowFixMe, + getComponent?: $FlowFixMe, + getComponents?: $FlowFixMe, + }; + + static createRouteFromReactElement(element, parentRoute) { + /* istanbul ignore else: sanity check */ + if (parentRoute) { + parentRoute.indexRoute = createRouteFromReactElement(element) + } else { + warning( + false, + 'An does not make sense at the root of your route config' + ) + } + } + + static propTypes = { + path: falsy, + component, + components, + getComponent: func, + getComponents: func + }; + + /* istanbul ignore next: sanity check */ + render() { + invariant( + false, + ' elements are for router configuration only and should not be rendered' + ) + } +} + +export default IndexRoute diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow3.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow3.input.js new file mode 100644 index 0000000..f42dfd4 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow3.input.js @@ -0,0 +1,47 @@ +/* @flow */ + +var React = require('react'); +var {PropTypes} = React; + +var getPropTypes = () => PropTypes.string; + +var myUnionPropType = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Message), +]); + +var spreadMe = { + optionalArray: PropTypes.array, + optionalBool: PropTypes.bool, +}; + +var optionalFuncShortHand = PropTypes.func; + +var Component = React.createClass({ + propTypes: { + ...spreadMe, + optionalFuncShortHand, + optionalNumber: 1 + 1 === 2 ? PropTypes.number : PropTypes.string, + optionalObject: PropTypes.object, + optionalString: getPropTypes(), + optionalNode: PropTypes.node, + optionalElement: PropTypes.element, + optionalMessage: PropTypes.instanceOf(Message), + optionalEnum: PropTypes.oneOf(['News', 'Photos', 1, true, null]), + optionalUnion: myUnionPropType, + optionalArrayOf: PropTypes.arrayOf(PropTypes.number), + optionalObjectOf: PropTypes.objectOf(PropTypes.number), + optionalObjectWithShape: PropTypes.shape({ + color: PropTypes.string, + }), + requiredFunc: PropTypes.func.isRequired, + requiredAny: PropTypes.any.isRequired, + }, + + render: function() { + return ( +
    type safety
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow3.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow3.output.js new file mode 100644 index 0000000..932b233 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow3.output.js @@ -0,0 +1,64 @@ +/* @flow */ + +var React = require('react'); +var {PropTypes} = React; + +var getPropTypes = () => PropTypes.string; + +var myUnionPropType = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Message), +]); + +var spreadMe = { + optionalArray: PropTypes.array, + optionalBool: PropTypes.bool, +}; + +var optionalFuncShortHand = PropTypes.func; + +class Component extends React.Component { + props: { + optionalFuncShortHand?: $FlowFixMe, + optionalNumber?: $FlowFixMe, + optionalObject?: Object, + optionalString?: $FlowFixMe, + optionalNode?: $FlowFixMe, + optionalElement?: $FlowFixMe, + optionalMessage?: Message, + optionalEnum?: 'News' | 'Photos' | 1 | true | null, + optionalUnion?: $FlowFixMe, + optionalArrayOf?: Array, + optionalObjectOf?: {[key: string]: number}, + optionalObjectWithShape?: {color?: string}, + requiredFunc: Function, + requiredAny: any, + }; + + static propTypes = { + ...spreadMe, + optionalFuncShortHand, + optionalNumber: 1 + 1 === 2 ? PropTypes.number : PropTypes.string, + optionalObject: PropTypes.object, + optionalString: getPropTypes(), + optionalNode: PropTypes.node, + optionalElement: PropTypes.element, + optionalMessage: PropTypes.instanceOf(Message), + optionalEnum: PropTypes.oneOf(['News', 'Photos', 1, true, null]), + optionalUnion: myUnionPropType, + optionalArrayOf: PropTypes.arrayOf(PropTypes.number), + optionalObjectOf: PropTypes.objectOf(PropTypes.number), + optionalObjectWithShape: PropTypes.shape({ + color: PropTypes.string, + }), + requiredFunc: PropTypes.func.isRequired, + requiredAny: PropTypes.any.isRequired, + }; + + render() { + return ( +
    type safety
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow4.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow4.input.js new file mode 100644 index 0000000..9694eb6 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow4.input.js @@ -0,0 +1,44 @@ +/* @flow */ + +var React = require('react'); +var {PropTypes} = React; + +var myUnionPropType = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Message), +]); + +var spreadMe = { + optionalArray: PropTypes.array, + optionalBool: PropTypes.bool, +}; + +var optionalFuncShortHand = PropTypes.func; + +var Component = React.createClass({ + propTypes: Object.assign({}, { + ...spreadMe, + optionalFuncShortHand, + optionalNumber: PropTypes.number, + optionalObject: PropTypes.object, + }), + + render: function() { + return ( +
    type safety
    + ); + }, +}); + +var thatPropTypes = {}; + +var Component2 = React.createClass({ + propTypes: thatPropTypes, + + render: function() { + return ( +
    type safety
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow4.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow4.output.js new file mode 100644 index 0000000..a25c625 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow4.output.js @@ -0,0 +1,44 @@ +/* @flow */ + +var React = require('react'); +var {PropTypes} = React; + +var myUnionPropType = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Message), +]); + +var spreadMe = { + optionalArray: PropTypes.array, + optionalBool: PropTypes.bool, +}; + +var optionalFuncShortHand = PropTypes.func; + +class Component extends React.Component { + static propTypes = Object.assign({}, { + ...spreadMe, + optionalFuncShortHand, + optionalNumber: PropTypes.number, + optionalObject: PropTypes.object, + }); + + render() { + return ( +
    type safety
    + ); + } +} + +var thatPropTypes = {}; + +class Component2 extends React.Component { + static propTypes = thatPropTypes; + + render() { + return ( +
    type safety
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow5.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow5.input.js new file mode 100644 index 0000000..3abae35 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow5.input.js @@ -0,0 +1,40 @@ +/* @flow */ + +var React = require('react'); + +type SomeStuff = { // TypeParameter + fetch: () => Promise, +}; + +var Component = React.createClass({ + statics: { + notTyped: true, + nothing: (null: null), // NullTypeAnnotation + numberOrBool: (true: number | boolean), + logger: (x: any): void => { console.log(x); }, + logger2: function(x: any): void { + console.log(x); + }, + }, + + notTyped: true, + foo: (12: number), + bar: ('2000': string), + handleClick: (null: ?(evt: any) => void), + + doStuff: function(x: number, y: boolean): boolean { + return y && (x > 0); + }, + + componentDidMount: function() { + this.handleClick = function(e) { + console.log(e); + }; + }, + + render: function() { + return ( +
    {this.foo}
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow5.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow5.output.js new file mode 100644 index 0000000..ca00112 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow5.output.js @@ -0,0 +1,39 @@ +/* @flow */ + +var React = require('react'); + +type SomeStuff
    = { // TypeParameter + fetch: () => Promise, +}; + +class Component extends React.Component { + static notTyped = true; + static nothing: null = null; // NullTypeAnnotation + static numberOrBool: number | boolean = true; + static logger = (x: any): void => { console.log(x); }; + + static logger2(x: any): void { + console.log(x); + } + + notTyped = true; + foo: number = 12; + bar: string = '2000'; + handleClick: ?(evt: any) => void = null; + + doStuff = (x: number, y: boolean): boolean => { + return y && (x > 0); + }; + + componentDidMount() { + this.handleClick = function(e) { + console.log(e); + }; + } + + render() { + return ( +
    {this.foo}
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow6.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow6.input.js new file mode 100644 index 0000000..b008f81 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow6.input.js @@ -0,0 +1,41 @@ +/* @flow */ + +var React = require('react'); + +const justNeedKeys = { + a: 12, + b: 23, +}; + +var Component = React.createClass({ + propTypes: { + optionalMessage: React.PropTypes.instanceOf(Message), + optionalMessageOops: React.PropTypes.instanceOf(foo()), + optionalEnum: React.PropTypes.oneOf(Object.keys(justNeedKeys)), + optionalEnumOops: React.PropTypes.oneOf(bar), + optionalUnion: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + React.PropTypes.instanceOf(Message), + ]), + optionalUnionOops: React.PropTypes.oneOfType(foo()), + optionalUnionOops2: React.PropTypes.oneOfType(Bar), + optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), + optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), + optionalObjectWithShape: React.PropTypes.shape({ + color: React.PropTypes.string, + fontSize: foo, + name: bla(), + }), + optionalObjectWithShapeOops: React.PropTypes.shape(foo()), + optionalObjectWithShapeOops2: React.PropTypes.shape(bla), + 'is-literal-cool': React.PropTypes.bool, + 'well-fine': React.PropTypes.number.isRequired, + }, + + render: function() { + return ( +
    type safety
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow6.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow6.output.js new file mode 100644 index 0000000..1ac3ff0 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow6.output.js @@ -0,0 +1,62 @@ +/* @flow */ + +var React = require('react'); + +const justNeedKeys = { + a: 12, + b: 23, +}; + +class Component extends React.Component { + props: { + optionalMessage?: Message, + optionalMessageOops?: $FlowFixMe, + optionalEnum?: $FlowFixMe, + optionalEnumOops?: $FlowFixMe, + optionalUnion?: string | number | Message, + optionalUnionOops?: $FlowFixMe, + optionalUnionOops2?: $FlowFixMe, + optionalArrayOf?: Array, + optionalObjectOf?: {[key: string]: number}, + optionalObjectWithShape?: { + color?: string, + fontSize?: $FlowFixMe, + name?: $FlowFixMe, + }, + optionalObjectWithShapeOops?: $FlowFixMe, + optionalObjectWithShapeOops2?: $FlowFixMe, + 'is-literal-cool'?: boolean, + 'well-fine': number, + }; + + static propTypes = { + optionalMessage: React.PropTypes.instanceOf(Message), + optionalMessageOops: React.PropTypes.instanceOf(foo()), + optionalEnum: React.PropTypes.oneOf(Object.keys(justNeedKeys)), + optionalEnumOops: React.PropTypes.oneOf(bar), + optionalUnion: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + React.PropTypes.instanceOf(Message), + ]), + optionalUnionOops: React.PropTypes.oneOfType(foo()), + optionalUnionOops2: React.PropTypes.oneOfType(Bar), + optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), + optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), + optionalObjectWithShape: React.PropTypes.shape({ + color: React.PropTypes.string, + fontSize: foo, + name: bla(), + }), + optionalObjectWithShapeOops: React.PropTypes.shape(foo()), + optionalObjectWithShapeOops2: React.PropTypes.shape(bla), + 'is-literal-cool': React.PropTypes.bool, + 'well-fine': React.PropTypes.number.isRequired, + }; + + render() { + return ( +
    type safety
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow7.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow7.input.js new file mode 100644 index 0000000..b008f81 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow7.input.js @@ -0,0 +1,41 @@ +/* @flow */ + +var React = require('react'); + +const justNeedKeys = { + a: 12, + b: 23, +}; + +var Component = React.createClass({ + propTypes: { + optionalMessage: React.PropTypes.instanceOf(Message), + optionalMessageOops: React.PropTypes.instanceOf(foo()), + optionalEnum: React.PropTypes.oneOf(Object.keys(justNeedKeys)), + optionalEnumOops: React.PropTypes.oneOf(bar), + optionalUnion: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + React.PropTypes.instanceOf(Message), + ]), + optionalUnionOops: React.PropTypes.oneOfType(foo()), + optionalUnionOops2: React.PropTypes.oneOfType(Bar), + optionalArrayOf: React.PropTypes.arrayOf(React.PropTypes.number), + optionalObjectOf: React.PropTypes.objectOf(React.PropTypes.number), + optionalObjectWithShape: React.PropTypes.shape({ + color: React.PropTypes.string, + fontSize: foo, + name: bla(), + }), + optionalObjectWithShapeOops: React.PropTypes.shape(foo()), + optionalObjectWithShapeOops2: React.PropTypes.shape(bla), + 'is-literal-cool': React.PropTypes.bool, + 'well-fine': React.PropTypes.number.isRequired, + }, + + render: function() { + return ( +
    type safety
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-flow7.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-flow7.output.js new file mode 100644 index 0000000..a30533b --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-flow7.output.js @@ -0,0 +1,37 @@ +/* @flow */ + +var React = require('react'); + +const justNeedKeys = { + a: 12, + b: 23, +}; + +class Component extends React.Component { + props: { + optionalMessage?: Message, + optionalMessageOops?: $FlowFixMe, + optionalEnum?: $FlowFixMe, + optionalEnumOops?: $FlowFixMe, + optionalUnion?: string | number | Message, + optionalUnionOops?: $FlowFixMe, + optionalUnionOops2?: $FlowFixMe, + optionalArrayOf?: Array, + optionalObjectOf?: {[key: string]: number}, + optionalObjectWithShape?: { + color?: string, + fontSize?: $FlowFixMe, + name?: $FlowFixMe, + }, + optionalObjectWithShapeOops?: $FlowFixMe, + optionalObjectWithShapeOops2?: $FlowFixMe, + 'is-literal-cool'?: boolean, + 'well-fine': number, + }; + + render() { + return ( +
    type safety
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-initial-state.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-initial-state.input.js new file mode 100644 index 0000000..bb18702 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-initial-state.input.js @@ -0,0 +1,335 @@ +/* @flow */ + +import React from 'React'; + +type SomeState = {foo: string}; + +// only needs props +var MyComponent = React.createClass({ + getInitialState: function(): {heyoo: number} { + var x = this.props.foo; + return { + heyoo: 23, + }; + }, + + foo: function(): void { + this.setState({heyoo: 24}); + }, +}); + +var ComponentWithBothPropsAndContextAccess = React.createClass({ + contextTypes: { + name: React.PropTypes.string, + }, + + // we actually don't need a constructor here since this will be + // initialized after a proper super(props, context) call. + // in other words, `this` will be ready when it reaches here. + getInitialState: function() { + return { + foo: this.props.foo, + bar: this.context.bar, + }; + }, + + render: function() { + return ( +
    {this.context.name}
    + ); + }, +}); + +const App = React.createClass({ + getInitialState(): SomeState { + const state = this.calculateState(); // _might_ use `this.context` + return state; + }, + calculateState() { + return { color: this.context.color }; + }, + render() { + return
    ; + }, +}); + +const App2 = React.createClass({ + getInitialState() { + const state = { + whatever: this.context.whatever, // needs context + }; + return state; + }, + render() { + return
    ; + }, +}); + +App.contextTypes = { + whatever: React.PropTypes.object, +}; + +var MyComponent2 = React.createClass({ + getInitialState: function() { + var x = this.props.foo.bar.wow.so.deep; + return { + heyoo: 23, + }; + }, + + foo: function(): void { + this.setState({heyoo: 24}); + }, +}); + +const getContextFromInstance = (x) => x.context; // meh + +var MyComponent3 = React.createClass({ + getInitialState: function() { + var x = getContextFromInstance(this); // `this` is referenced alone + return { + heyoo: x, + }; + }, + + foo: function(): void { + this.setState({heyoo: 24}); + }, +}); + +// we are not sure what you'll need from `this`, +// so it's safe to defer `state`'s initialization +var MyComponent4 = React.createClass({ + getInitialState: function() { + return { + heyoo: getContextFromInstance(this), + }; + }, + + foo: function(): void { + this.setState({heyoo: 24}); + }, +}); + +// but only accessing `this.props` and/or `this.context` is safe +var MyComponent5 = React.createClass({ + getInitialState: function() { + return { + heyoo: getContextFromInstance(this.props), + }; + }, + + foo: function(): void { + this.setState({heyoo: 24}); + }, +}); + +// intense control flow testing +var Loader = React.createClass({ + getInitialState() { + if (this.props.stuff) { + return {x: 1}; + } else if (this.props.thing) { + return {x: 2}; + } + switch (this.props.wow) { + case 1: + return this.props.lol ? + {x: 3} : + this.whatever(this.props); + } + for (let i = 0; i < 100; i++) { + if (i === 20) { + return {x: i}; + } + } + + try { + doSomeThingReallyBad(); + } catch (e) { + return {error: e}; + } + + return this.lol(); + }, + + render() { + return null; + }, +}); + +var FunctionDeclarationInGetInitialState = React.createClass({ + getInitialState() { + function func() { + var x = 1; + return x; // dont change me + } + + const foo = () => { + return 120; // dont change me + }; + + var q = function() { + return 100; // dont change me + }; + + return { + x: func(), + y: foo(), + z: q(), + }; + }, + + render() { + return null; + }, +}); + +var DeferStateInitialization = React.createClass({ + getInitialState() { + return {x: this.something}; + }, + + something: 42, + + render() { + return
    ; + }, +}); + +var helper = () => {}; + +// fallback +var PassGetInitialState = React.createClass({ + getInitialState() { + return this.lol(); + }, + + helper1: function() { + helper(this.getInitialState); + }, + + render() { + return null; + }, +}); + +// fallback +var UseGetInitialState = React.createClass({ + getInitialState() { + return this.lol(); + }, + + helper2() { + this.setState(this.getInitialState()); + }, + + render() { + return null; + }, +}); + +// fallback +var UseArguments = React.createClass({ + helper() { + console.log(arguments); + }, + + render() { + return null; + }, +}); + +// fallback +var ShadowingIssue = React.createClass({ + getInitialState() { + const props = { x: 123 }; + return { x: props.x }; + }, + + render() { + return null; + }, +}); + +// will remove unnecessary bindings +var ShadowingButFine = React.createClass({ + getInitialState() { + const props = this.props; + const context = this.context; + return { x: props.x + context.x }; + }, + + render() { + return null; + }, +}); + +// move type annotations +var WithSimpleType = React.createClass({ + getInitialState(): Object { + return { + x: 12, + y: 13, + z: 14, + }; + }, + + render() { + return null; + }, +}); + +var WithLongType = React.createClass({ + getInitialState(): {name: string, age: number, counter: number} { + return { + name: 'Michael', + age: 23, + count: 6, + }; + }, + + render() { + return null; + }, +}); + +var WithMultiLineType = React.createClass({ + getInitialState(): { + nameLists: Array>, + age?: ?number, + counter?: ?number, + } { + return { + nameLists: [['James']], + count: 1400, + foo: 'bar', + }; + }, + + render() { + return null; + }, +}); + +var WithArrowFunction = React.createClass({ + getInitialState: (): {heyoo: number} => { + return { + heyoo: 23, + }; + }, + + render() { + return null; + }, +}); + +var WithArrowFunctionAndObject = React.createClass({ + getInitialState: (): {heyoo: number} => ({ + heyoo: 23, + }), + + render() { + return null; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-initial-state.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-initial-state.output.js new file mode 100644 index 0000000..ee332e3 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-initial-state.output.js @@ -0,0 +1,362 @@ +/* @flow */ + +import React from 'React'; + +import createReactClass from 'create-react-class'; + +type SomeState = {foo: string}; + +// only needs props +class MyComponent extends React.Component { + state: {heyoo: number}; + + constructor(props) { + super(props); + var x = props.foo; + + this.state = { + heyoo: 23, + }; + } + + foo = (): void => { + this.setState({heyoo: 24}); + }; +} + +class ComponentWithBothPropsAndContextAccess extends React.Component { + static contextTypes = { + name: React.PropTypes.string, + }; + + // we actually don't need a constructor here since this will be + // initialized after a proper super(props, context) call. + // in other words, `this` will be ready when it reaches here. + state = { + foo: this.props.foo, + bar: this.context.bar, + }; + + render() { + return ( +
    {this.context.name}
    + ); + } +} + +class App extends React.Component { + state: SomeState; + + constructor(props, context) { + super(props, context); + const state = this.calculateState(); // _might_ use `this.context` + this.state = state; + } + + calculateState = () => { + return { color: this.context.color }; + }; + + render() { + return
    ; + } +} + +class App2 extends React.Component { + state: *; + + constructor(props, context) { + super(props, context); + const state = { + whatever: context.whatever, // needs context + }; + this.state = state; + } + + render() { + return
    ; + } +} + +App.contextTypes = { + whatever: React.PropTypes.object, +}; + +class MyComponent2 extends React.Component { + state: *; + + constructor(props) { + super(props); + var x = props.foo.bar.wow.so.deep; + + this.state = { + heyoo: 23, + }; + } + + foo = (): void => { + this.setState({heyoo: 24}); + }; +} + +const getContextFromInstance = (x) => x.context; // meh + +class MyComponent3 extends React.Component { + state: *; + + constructor(props, context) { + super(props, context); + var x = getContextFromInstance(this); // `this` is referenced alone + + this.state = { + heyoo: x, + }; + } + + foo = (): void => { + this.setState({heyoo: 24}); + }; +} + +// we are not sure what you'll need from `this`, +// so it's safe to defer `state`'s initialization +class MyComponent4 extends React.Component { + foo = (): void => { + this.setState({heyoo: 24}); + }; + + state = { + heyoo: getContextFromInstance(this), + }; +} + +// but only accessing `this.props` and/or `this.context` is safe +class MyComponent5 extends React.Component { + state = { + heyoo: getContextFromInstance(this.props), + }; + + foo = (): void => { + this.setState({heyoo: 24}); + }; +} + +// intense control flow testing +class Loader extends React.Component { + state: *; + + constructor(props, context) { + super(props, context); + if (props.stuff) { + this.state = {x: 1}; + return; + } else if (props.thing) { + this.state = {x: 2}; + return; + } + switch (props.wow) { + case 1: + this.state = props.lol ? + {x: 3} : + this.whatever(props); + + return; + } + for (let i = 0; i < 100; i++) { + if (i === 20) { + this.state = {x: i}; + return; + } + } + + try { + doSomeThingReallyBad(); + } catch (e) { + this.state = {error: e}; + return; + } + + this.state = this.lol(); + } + + render() { + return null; + } +} + +class FunctionDeclarationInGetInitialState extends React.Component { + state: *; + + constructor(props) { + super(props); + function func() { + var x = 1; + return x; // dont change me + } + + const foo = () => { + return 120; // dont change me + }; + + var q = function() { + return 100; // dont change me + }; + + this.state = { + x: func(), + y: foo(), + z: q(), + }; + } + + render() { + return null; + } +} + +class DeferStateInitialization extends React.Component { + something = 42; + state = {x: this.something}; + + render() { + return
    ; + } +} + +var helper = () => {}; + +// fallback +var PassGetInitialState = createReactClass({ + displayName: 'PassGetInitialState', + + getInitialState() { + return this.lol(); + }, + + helper1: function() { + helper(this.getInitialState); + }, + + render() { + return null; + }, +}); + +// fallback +var UseGetInitialState = createReactClass({ + displayName: 'UseGetInitialState', + + getInitialState() { + return this.lol(); + }, + + helper2() { + this.setState(this.getInitialState()); + }, + + render() { + return null; + }, +}); + +// fallback +var UseArguments = createReactClass({ + displayName: 'UseArguments', + + helper() { + console.log(arguments); + }, + + render() { + return null; + }, +}); + +// fallback +var ShadowingIssue = createReactClass({ + displayName: 'ShadowingIssue', + + getInitialState() { + const props = { x: 123 }; + return { x: props.x }; + }, + + render() { + return null; + }, +}); + +// will remove unnecessary bindings +class ShadowingButFine extends React.Component { + state: *; + + constructor(props, context) { + super(props, context); + this.state = { x: props.x + context.x }; + } + + render() { + return null; + } +} + +// move type annotations +class WithSimpleType extends React.Component { + state: Object = { + x: 12, + y: 13, + z: 14, + }; + + render() { + return null; + } +} + +class WithLongType extends React.Component { + state: {name: string, age: number, counter: number} = { + name: 'Michael', + age: 23, + count: 6, + }; + + render() { + return null; + } +} + +class WithMultiLineType extends React.Component { + state: { + nameLists: Array>, + age?: ?number, + counter?: ?number, + } = { + nameLists: [['James']], + count: 1400, + foo: 'bar', + }; + + render() { + return null; + } +} + +class WithArrowFunction extends React.Component { + state: {heyoo: number} = { + heyoo: 23, + }; + + render() { + return null; + } +} + +class WithArrowFunctionAndObject extends React.Component { + state: {heyoo: number} = { + heyoo: 23, + }; + + render() { + return null; + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-no-conversion.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-no-conversion.input.js new file mode 100644 index 0000000..d12952a --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-no-conversion.input.js @@ -0,0 +1,9 @@ +'use strict'; + +var React = require('React'); + +var Component = React.createClass({ + render() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-no-conversion.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-no-conversion.output.js new file mode 100644 index 0000000..82768d7 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-no-conversion.output.js @@ -0,0 +1,13 @@ +'use strict'; + +var React = require('React'); + +var createReactClass = require('create-react-class'); + +var Component = createReactClass({ + displayName: 'Component', + + render() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-no-display-name.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-no-display-name.input.js new file mode 100644 index 0000000..a2f65e0 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-no-display-name.input.js @@ -0,0 +1,10 @@ +'use strict'; + +var React = require('React'); + +var Component = React.createClass({ + mixins: [{}], + render() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-no-display-name.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-no-display-name.output.js new file mode 100644 index 0000000..1385b8f --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-no-display-name.output.js @@ -0,0 +1,12 @@ +'use strict'; + +var React = require('React'); + +var createReactClass = require('create-react-class'); + +var Component = createReactClass({ + mixins: [{}], + render() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-property-field.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-property-field.input.js new file mode 100644 index 0000000..e6ba400 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-property-field.input.js @@ -0,0 +1,28 @@ +const React = require('react'); + +const Component1 = React.createClass({ + statics: { + booleanPrim: true, + numberPrim: 12, + stringPrim: 'foo', + nullPrim: null, + undefinedPrim: undefined, + }, + booleanPrim: true, + numberPrim: 12, + stringPrim: 'foo', + nullPrim: null, + undefinedPrim: undefined, + + foobar: function() { + return 123; + }, + + componentDidMount: function() { + console.log('hello'); + }, + + render: function() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-property-field.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-property-field.output.js new file mode 100644 index 0000000..34305a2 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-property-field.output.js @@ -0,0 +1,26 @@ +const React = require('react'); + +class Component1 extends React.Component { + static booleanPrim = true; + static numberPrim = 12; + static stringPrim = 'foo'; + static nullPrim = null; + static undefinedPrim = undefined; + booleanPrim = true; + numberPrim = 12; + stringPrim = 'foo'; + nullPrim = null; + undefinedPrim = undefined; + + foobar = () => { + return 123; + }; + + componentDidMount() { + console.log('hello'); + } + + render() { + return
    ; + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-prune-react.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react.input.js new file mode 100644 index 0000000..478746e --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react.input.js @@ -0,0 +1,16 @@ +'use strict'; + +import React from 'React'; + +const SomeMixin = { + componentDidMount() { + console.log('did mount'); + }, +}; + +export default React.createClass({ + mixins: [SomeMixin], + render: function() { + return null; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-prune-react.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react.output.js new file mode 100644 index 0000000..85ab457 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react.output.js @@ -0,0 +1,18 @@ +'use strict'; + +import createReactClass from 'create-react-class'; + +const SomeMixin = { + componentDidMount() { + console.log('did mount'); + }, +}; + +export default createReactClass({ + displayName: 'class-prune-react.input', + mixins: [SomeMixin], + + render: function() { + return null; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-prune-react2.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react2.input.js new file mode 100644 index 0000000..ad3d240 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react2.input.js @@ -0,0 +1,16 @@ +'use strict'; + +import React from 'React'; + +const SomeMixin = { + componentDidMount() { + console.log('did mount'); + }, +}; + +export default React.createClass({ + mixins: [SomeMixin], + render: function() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-prune-react2.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react2.output.js new file mode 100644 index 0000000..53548b9 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react2.output.js @@ -0,0 +1,20 @@ +'use strict'; + +import React from 'React'; + +import createReactClass from 'create-react-class'; + +const SomeMixin = { + componentDidMount() { + console.log('did mount'); + }, +}; + +export default createReactClass({ + displayName: 'class-prune-react2.input', + mixins: [SomeMixin], + + render: function() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-prune-react3.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react3.input.js new file mode 100644 index 0000000..69fb6b1 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react3.input.js @@ -0,0 +1,19 @@ +'use strict'; + +import React, {PropTypes} from 'React'; + +const SomeMixin = { + componentDidMount() { + console.log('did mount'); + }, +}; + +export default React.createClass({ + mixins: [SomeMixin], + propTypes: { + foo: PropTypes.string, + }, + render: function() { + return null; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-prune-react3.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react3.output.js new file mode 100644 index 0000000..9580454 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react3.output.js @@ -0,0 +1,24 @@ +'use strict'; + +import {PropTypes} from 'React'; + +import createReactClass from 'create-react-class'; + +const SomeMixin = { + componentDidMount() { + console.log('did mount'); + }, +}; + +export default createReactClass({ + displayName: 'class-prune-react3.input', + mixins: [SomeMixin], + + propTypes: { + foo: PropTypes.string, + }, + + render: function() { + return null; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-prune-react4.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react4.input.js new file mode 100644 index 0000000..2f68eb1 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react4.input.js @@ -0,0 +1,19 @@ +'use strict'; + +import React, {PropTypes} from 'React'; + +const SomeMixin = { + componentDidMount() { + console.log('did mount'); + }, +}; + +export default React.createClass({ + mixins: [SomeMixin], + propTypes: { + foo: PropTypes.string, + }, + render: function() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-prune-react4.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react4.output.js new file mode 100644 index 0000000..8bb26a9 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-prune-react4.output.js @@ -0,0 +1,24 @@ +'use strict'; + +import React, {PropTypes} from 'React'; + +import createReactClass from 'create-react-class'; + +const SomeMixin = { + componentDidMount() { + console.log('did mount'); + }, +}; + +export default createReactClass({ + displayName: 'class-prune-react4.input', + mixins: [SomeMixin], + + propTypes: { + foo: PropTypes.string, + }, + + render: function() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin1.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin1.input.js new file mode 100644 index 0000000..381a7e7 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin1.input.js @@ -0,0 +1,19 @@ +// dont remove me +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); +var React = require('React'); + +var ComponentWithOnlyPureRenderMixin = React.createClass({ + mixins: [ReactComponentWithPureRenderMixin], + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin1.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin1.output.js new file mode 100644 index 0000000..741db3c --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin1.output.js @@ -0,0 +1,14 @@ +// dont remove me +var React = require('React'); + +class ComponentWithOnlyPureRenderMixin extends React.PureComponent { + state = { + counter: this.props.initialNumber + 1, + }; + + render() { + return ( +
    {this.state.counter}
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin2.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin2.input.js new file mode 100644 index 0000000..820b1e3 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin2.input.js @@ -0,0 +1,29 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @typechecks + * @flow + */ +import WhateverYouCallIt from 'react-addons-pure-render-mixin'; +import React from 'React'; +import dontPruneMe from 'foobar'; + +var ComponentWithOnlyPureRenderMixin = React.createClass({ + mixins: [WhateverYouCallIt], + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + dontPruneMe(); + return ( +
    {this.state.counter}
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin2.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin2.output.js new file mode 100644 index 0000000..559a31e --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin2.output.js @@ -0,0 +1,24 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @typechecks + * @flow + */ +import React from 'React'; +import dontPruneMe from 'foobar'; + +class ComponentWithOnlyPureRenderMixin extends React.PureComponent { + state = { + counter: this.props.initialNumber + 1, + }; + + render() { + dontPruneMe(); + return ( +
    {this.state.counter}
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin3.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin3.input.js new file mode 100644 index 0000000..fd7e2e1 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin3.input.js @@ -0,0 +1,20 @@ +// for this file we disable the `pure-component` option +// so we should not convert to a plain class +var React = require('React'); +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); + +var ComponentWithOnlyPureRenderMixin = React.createClass({ + mixins: [ReactComponentWithPureRenderMixin], + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin3.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin3.output.js new file mode 100644 index 0000000..d40d339 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin3.output.js @@ -0,0 +1,22 @@ +// for this file we disable the `pure-component` option +// so we should not convert to a plain class +var React = require('React'); +var createReactClass = require('create-react-class'); +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); + +var ComponentWithOnlyPureRenderMixin = createReactClass({ + displayName: 'ComponentWithOnlyPureRenderMixin', + mixins: [ReactComponentWithPureRenderMixin], + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin4.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin4.input.js new file mode 100644 index 0000000..a1e7054 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin4.input.js @@ -0,0 +1,36 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @fbt {"foo": "bar"} + * @flow + * @typechecks + */ + +'use strict'; + +const React = require('React'); +const ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); + +/** + * just a description here + */ +const HelloGuys = React.createClass({ + mixins: [ + ReactComponentWithPureRenderMixin, + ], + + propTypes: {}, + + render(): ReactElement { + return ( +
    + wassup +
    + ); + }, +}); + +module.exports = HelloGuys; diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin4.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin4.output.js new file mode 100644 index 0000000..5902dfb --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin4.output.js @@ -0,0 +1,32 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @fbt {"foo": "bar"} + * @flow + * @typechecks + */ + +'use strict'; + +const React = require('React'); + +/** + * just a description here + */ +class HelloGuys extends React.PureComponent { + props: {}; + static propTypes = {}; + + render(): ReactElement { + return ( +
    + wassup +
    + ); + } +} + +module.exports = HelloGuys; diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin5.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin5.input.js new file mode 100644 index 0000000..b098172 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin5.input.js @@ -0,0 +1,19 @@ +// dont remove me +var React = require('React'), + ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); + +var ComponentWithOnlyPureRenderMixin = React.createClass({ + mixins: [ReactComponentWithPureRenderMixin], + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin5.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin5.output.js new file mode 100644 index 0000000..741db3c --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-pure-mixin5.output.js @@ -0,0 +1,14 @@ +// dont remove me +var React = require('React'); + +class ComponentWithOnlyPureRenderMixin extends React.PureComponent { + state = { + counter: this.props.initialNumber + 1, + }; + + render() { + return ( +
    {this.state.counter}
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-test2.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-test2.input.js new file mode 100644 index 0000000..ae54847 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-test2.input.js @@ -0,0 +1,116 @@ +'use strict'; + +var React = require('React'); +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); +var FooBarMixin = require('FooBarMixin'); + +var ComponentWithNonSimpleInitialState = React.createClass({ + statics: { + iDontKnowWhyYouNeedThis: true, // but comment it + foo: 'bar', + dontBindMe: function(count: number): any { + return this; + }, + }, + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); + +// Comment +module.exports = React.createClass({ + propTypes: { + foo: React.PropTypes.bool, + }, + + getDefaultProps: function() { + return { + foo: 12, + }; + }, + + getInitialState: function() { // non-simple getInitialState + var data = 'bar'; + return { + bar: data, + }; + }, + + render: function() { + return
    ; + }, +}); + +var ComponentWithInconvertibleMixins = React.createClass({ + mixins: [ReactComponentWithPureRenderMixin, FooBarMixin], + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); + +var listOfInconvertibleMixins = [ReactComponentWithPureRenderMixin, FooBarMixin]; + +var ComponentWithInconvertibleMixins2 = React.createClass({ + mixins: listOfInconvertibleMixins, + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); + +// taken from https://facebook.github.io/react/docs/context.html#updating-context +var MediaQuery = React.createClass({ + childContextTypes: { + type: React.PropTypes.string, + }, + + getInitialState: function() { + return {type:'desktop'}; + }, + + getChildContext: function() { + return {type: this.state.type}; + }, + + componentDidMount: function() { + const checkMediaQuery = () => { + const type = window.matchMedia('(min-width: 1025px)').matches ? 'desktop' : 'mobile'; + if (type !== this.state.type) { + this.setState({type}); + } + }; + + window.addEventListener('resize', checkMediaQuery); + checkMediaQuery(); + }, + + render: function() { + return this.props.children; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-test2.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-test2.output.js new file mode 100644 index 0000000..50ab376 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-test2.output.js @@ -0,0 +1,115 @@ +'use strict'; + +var React = require('React'); +var createReactClass = require('create-react-class'); +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); +var FooBarMixin = require('FooBarMixin'); + +class ComponentWithNonSimpleInitialState extends React.Component { + static iDontKnowWhyYouNeedThis = true; // but comment it + static foo = 'bar'; + + static dontBindMe(count: number): any { + return this; + } + + state = { + counter: this.props.initialNumber + 1, + }; + + render() { + return ( +
    {this.state.counter}
    + ); + } +} + +// Comment +module.exports = class extends React.Component { + static propTypes = { + foo: React.PropTypes.bool, + }; + + static defaultProps = { + foo: 12, + }; + + constructor(props) { + super(props); + // non-simple getInitialState + var data = 'bar'; + + this.state = { + bar: data, + }; + } + + render() { + return
    ; + } +}; + +var ComponentWithInconvertibleMixins = createReactClass({ + displayName: 'ComponentWithInconvertibleMixins', + mixins: [ReactComponentWithPureRenderMixin, FooBarMixin], + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); + +var listOfInconvertibleMixins = [ReactComponentWithPureRenderMixin, FooBarMixin]; + +var ComponentWithInconvertibleMixins2 = createReactClass({ + displayName: 'ComponentWithInconvertibleMixins2', + mixins: listOfInconvertibleMixins, + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); + +// taken from https://facebook.github.io/react/docs/context.html#updating-context +class MediaQuery extends React.Component { + static childContextTypes = { + type: React.PropTypes.string, + }; + + state = {type:'desktop'}; + + getChildContext() { + return {type: this.state.type}; + } + + componentDidMount() { + const checkMediaQuery = () => { + const type = window.matchMedia('(min-width: 1025px)').matches ? 'desktop' : 'mobile'; + if (type !== this.state.type) { + this.setState({type}); + } + }; + + window.addEventListener('resize', checkMediaQuery); + checkMediaQuery(); + } + + render() { + return this.props.children; + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-top-comment.input.js b/codemods/legacy/transforms/__testfixtures__/class/class-top-comment.input.js new file mode 100644 index 0000000..b01b81c --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-top-comment.input.js @@ -0,0 +1,27 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @typechecks + * @flow + */ +var ReactComponentWithPureRenderMixin = require('ReactComponentWithPureRenderMixin'); +var React = require('React'); + +var ComponentWithOnlyPureRenderMixin = React.createClass({ + mixins: [ReactComponentWithPureRenderMixin], + + getInitialState: function() { + return { + counter: this.props.initialNumber + 1, + }; + }, + + render: function() { + return ( +
    {this.state.counter}
    + ); + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class-top-comment.output.js b/codemods/legacy/transforms/__testfixtures__/class/class-top-comment.output.js new file mode 100644 index 0000000..0da85b9 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class-top-comment.output.js @@ -0,0 +1,22 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @typechecks + * @flow + */ +var React = require('React'); + +class ComponentWithOnlyPureRenderMixin extends React.PureComponent { + state = { + counter: this.props.initialNumber + 1, + }; + + render() { + return ( +
    {this.state.counter}
    + ); + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/class.input.js b/codemods/legacy/transforms/__testfixtures__/class/class.input.js new file mode 100644 index 0000000..96a0d9d --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class.input.js @@ -0,0 +1,214 @@ +'use strict'; + +var React = require('React'); +var Relay = require('Relay'); + +var Image = require('Image.react'); + +// Class comment +var MyComponent2 = React.createClass({ + getDefaultProps: function(): Object { + return {a: 1}; + }, + foo: function(): void { + const x = (a: Object, b: string): void => {}; // This code cannot be parsed by Babel v5 + pass(this.foo); + this.forceUpdate(); + }, +}); + +var MyComponent3 = React.createClass({ + statics: { + someThing: 10, + funcThatDoesNothing: function(): void {}, + }, + propTypes: { + highlightEntities: React.PropTypes.bool, + linkifyEntities: React.PropTypes.bool, + text: React.PropTypes.shape({ + text: React.PropTypes.string, + ranges: React.PropTypes.array, + }).isRequired, + }, + + getDefaultProps: function() { + unboundFunc(); + return { + linkifyEntities: true, + highlightEntities: false, + }; + }, + + getInitialState: function() { + this.props.foo(); + return { + heyoo: 23, + }; + }, + + // comment here + _renderText: function(text: string): ReactElement { // say something + return ; + }, + + _renderImageRange: function(text: string, range): ReactElement { + var image = range.image; + if (image) { + return ( + + ); + } + return null; + }, + + autobindMe: function() {}, + okBindMe: function(): number { return 12; }, + + // Function comment + _renderRange: function(text: string, range, bla: Promise): ReactElement { + var self = this; + + self.okBindMe(); + call(self.autobindMe); + + var type = rage.type; + var {highlightEntities} = this.props; + + if (type === 'ImageAtRange') { + return this._renderImageRange(text, range); + } + + if (this.props.linkifyEntities) { + text = + + {text} + ; + } else { + text = {text}; + } + + return text; + }, + + /* This is a comment */ + render: function() { + var content = this.props.text; + return ( + + ); + }, +}); + +var MyComponent4 = React.createClass({ + foo: callMeMaybe(), + render: function() {}, +}); + +module.exports = Relay.createContainer(MyComponent, { + queries: { + me: Relay.graphql`this is not graphql`, + }, +}); + +var MyComponent5 = React.createClass({ + getDefaultProps: function() { + return { + thisIs: true, + andThisIs: false, + }; + }, + + statics: {}, + + getInitialState: function() { + return { + todos: [], + }; + }, + + renderTodo: function(): ReactElement { + return ( +
    + {this.state.todos.map((item) =>

    {item.text}

    )} +
    + ); + }, + + render: function() { + return ( +
    +

    TODOs

    + {this.renderTodo()} +
    + ); + }, +}); + +var GoodName = React.createClass({ + displayName: 'GoodName', + render() { + return
    ; + }, +}); + +var SingleArgArrowFunction = React.createClass({ + formatInt: function(/*number*/ num) /*string*/ { + return 'foobar'; + }, + render() { + return
    ; + }, +}); + +var mySpec = {}; +var NotAnObjectLiteral = React.createClass(mySpec); + +var WaitWhat = React.createClass(); + +var HasSpreadArgs = React.createClass({ + _helper: function(...args) { + return args; + }, + _helper2: function(a, b, c, ...args) { + return args.concat(a); + }, + _helper3: function(a: number, ...args: Array) { + return args.concat('' + a); + }, + render() { + return
    ; + }, +}); + +var HasDefaultArgs = React.createClass({ + _helper: function(foo = 12) { + return foo; + }, + _helper2: function({foo: number = 12, abc}, bar: string = 'hey', ...args: Array) { + return args.concat(foo, bar); + }, + render() { + return
    ; + }, +}); + +var ManyArgs = React.createClass({ + _helper: function(foo = 12) { + return foo; + }, + _helper2: function({foo: number = 12, abc}, bar: string = 'hey', x: number, y: number, ...args: Array) { + return args.concat(foo, bar); + }, + render() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/class.output.js b/codemods/legacy/transforms/__testfixtures__/class/class.output.js new file mode 100644 index 0000000..61e1065 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/class.output.js @@ -0,0 +1,225 @@ +'use strict'; + +var React = require('React'); +var createReactClass = require('create-react-class'); +var Relay = require('Relay'); + +var Image = require('Image.react'); + +// Class comment +class MyComponent2 extends React.Component { + static defaultProps = {a: 1}; + + foo = (): void => { + const x = (a: Object, b: string): void => {}; // This code cannot be parsed by Babel v5 + pass(this.foo); + this.forceUpdate(); + }; +} + +class MyComponent3 extends React.Component { + static someThing = 10; + static funcThatDoesNothing(): void {} + + static propTypes = { + highlightEntities: React.PropTypes.bool, + linkifyEntities: React.PropTypes.bool, + text: React.PropTypes.shape({ + text: React.PropTypes.string, + ranges: React.PropTypes.array, + }).isRequired, + }; + + static defaultProps = function() { + unboundFunc(); + return { + linkifyEntities: true, + highlightEntities: false, + }; + }(); + + constructor(props) { + super(props); + props.foo(); + + this.state = { + heyoo: 23, + }; + } + + // comment here + _renderText = (text: string): ReactElement => { // say something + return ; + }; + + _renderImageRange = (text: string, range): ReactElement => { + var image = range.image; + if (image) { + return ( + + ); + } + return null; + }; + + autobindMe = () => {}; + okBindMe = (): number => { return 12; }; + + // Function comment + _renderRange = (text: string, range, bla: Promise): ReactElement => { + var self = this; + + self.okBindMe(); + call(self.autobindMe); + + var type = rage.type; + var {highlightEntities} = this.props; + + if (type === 'ImageAtRange') { + return this._renderImageRange(text, range); + } + + if (this.props.linkifyEntities) { + text = + + {text} + ; + } else { + text = {text}; + } + + return text; + }; + + /* This is a comment */ + render() { + var content = this.props.text; + return ( + + ); + } +} + +var MyComponent4 = createReactClass({ + displayName: 'MyComponent4', + foo: callMeMaybe(), + render: function() {}, +}); + +module.exports = Relay.createContainer(MyComponent, { + queries: { + me: Relay.graphql`this is not graphql`, + }, +}); + +class MyComponent5 extends React.Component { + static defaultProps = { + thisIs: true, + andThisIs: false, + }; + + state = { + todos: [], + }; + + renderTodo = (): ReactElement => { + return ( +
    + {this.state.todos.map((item) =>

    {item.text}

    )} +
    + ); + }; + + render() { + return ( +
    +

    TODOs

    + {this.renderTodo()} +
    + ); + } +} + +class GoodName extends React.Component { + static displayName = 'GoodName'; + + render() { + return
    ; + } +} + +class SingleArgArrowFunction extends React.Component { + formatInt = (/*number*/ num) => /*string*/ { + return 'foobar'; + }; + + render() { + return
    ; + } +} + +var mySpec = {}; +var NotAnObjectLiteral = createReactClass(mySpec); + +var WaitWhat = createReactClass(); + +class HasSpreadArgs extends React.Component { + _helper = (...args) => { + return args; + }; + + _helper2 = (a, b, c, ...args) => { + return args.concat(a); + }; + + _helper3 = (a: number, ...args: Array) => { + return args.concat('' + a); + }; + + render() { + return
    ; + } +} + +class HasDefaultArgs extends React.Component { + _helper = (foo = 12) => { + return foo; + }; + + _helper2 = ({foo: number = 12, abc}, bar: string = 'hey', ...args: Array) => { + return args.concat(foo, bar); + }; + + render() { + return
    ; + } +} + +class ManyArgs extends React.Component { + _helper = (foo = 12) => { + return foo; + }; + + _helper2 = ( + {foo: number = 12, abc}, + bar: string = 'hey', + x: number, + y: number, + ...args: Array + ) => { + return args.concat(foo, bar); + }; + + render() { + return
    ; + } +} diff --git a/codemods/legacy/transforms/__testfixtures__/class/export-default-class.input.js b/codemods/legacy/transforms/__testfixtures__/class/export-default-class.input.js new file mode 100644 index 0000000..148843b --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/export-default-class.input.js @@ -0,0 +1,21 @@ +/*eslint-disable no-extra-semi*/ + +'use strict'; + +import React from 'React'; + +export default React.createClass({ + getInitialState: function() { + return { + foo: 'bar', + }; + }, + + propTypes: { + foo: React.PropTypes.string, + }, + + render: function() { + return
    ; + }, +}); diff --git a/codemods/legacy/transforms/__testfixtures__/class/export-default-class.output.js b/codemods/legacy/transforms/__testfixtures__/class/export-default-class.output.js new file mode 100644 index 0000000..23a7d26 --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/class/export-default-class.output.js @@ -0,0 +1,19 @@ +/*eslint-disable no-extra-semi*/ + +'use strict'; + +import React from 'React'; + +export default class extends React.Component { + static propTypes = { + foo: React.PropTypes.string, + }; + + state = { + foo: 'bar', + }; + + render() { + return
    ; + } +} diff --git a/codemods/create-element-to-jsx/tests/allow-member-expression/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-allow-member-expression.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/allow-member-expression/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-allow-member-expression.input.js diff --git a/codemods/create-element-to-jsx/tests/allow-member-expression/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-allow-member-expression.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/allow-member-expression/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-allow-member-expression.output.js diff --git a/codemods/create-element-to-jsx/tests/arg-spread/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-arg-spread.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/arg-spread/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-arg-spread.input.js diff --git a/codemods/create-element-to-jsx/tests/arg-spread/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-arg-spread.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/arg-spread/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-arg-spread.output.js diff --git a/codemods/create-element-to-jsx/tests/call-as-children/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-call-as-children.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/call-as-children/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-call-as-children.input.js diff --git a/codemods/create-element-to-jsx/tests/call-as-children/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-call-as-children.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/call-as-children/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-call-as-children.output.js diff --git a/codemods/create-element-to-jsx/tests/call-expression-as-prop/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-call-expression-as-prop.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/call-expression-as-prop/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-call-expression-as-prop.input.js diff --git a/codemods/create-element-to-jsx/tests/call-expression-as-prop/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-call-expression-as-prop.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/call-expression-as-prop/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-call-expression-as-prop.output.js diff --git a/codemods/create-element-to-jsx/tests/children-literal/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-literal.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/children-literal/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-literal.input.js diff --git a/codemods/create-element-to-jsx/tests/children-literal/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-literal.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/children-literal/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-literal.output.js diff --git a/codemods/create-element-to-jsx/tests/children-map/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-map.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/children-map/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-map.input.js diff --git a/codemods/create-element-to-jsx/tests/children-map/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-map.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/children-map/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-map.output.js diff --git a/codemods/create-element-to-jsx/tests/children-mixed-empty-string/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-mixed-empty-string.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/children-mixed-empty-string/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-mixed-empty-string.input.js diff --git a/codemods/create-element-to-jsx/tests/children-mixed-empty-string/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-mixed-empty-string.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/children-mixed-empty-string/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children-mixed-empty-string.output.js diff --git a/codemods/create-element-to-jsx/tests/children/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/children/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children.input.js diff --git a/codemods/create-element-to-jsx/tests/children/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/children/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-children.output.js diff --git a/codemods/create-element-to-jsx/tests/computed-component/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-computed-component.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/computed-component/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-computed-component.input.js diff --git a/codemods/create-element-to-jsx/tests/computed-component/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-computed-component.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/computed-component/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-computed-component.output.js diff --git a/codemods/create-element-to-jsx/tests/deep-nesting/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-deep-nesting.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/deep-nesting/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-deep-nesting.input.js diff --git a/codemods/create-element-to-jsx/tests/deep-nesting/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-deep-nesting.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/deep-nesting/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-deep-nesting.output.js diff --git a/codemods/create-element-to-jsx/tests/element-comment-positioning/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-element-comment-positioning.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/element-comment-positioning/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-element-comment-positioning.input.js diff --git a/codemods/create-element-to-jsx/tests/element-comment-positioning/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-element-comment-positioning.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/element-comment-positioning/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-element-comment-positioning.output.js diff --git a/codemods/create-element-to-jsx/tests/escaped-string/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-escaped-string.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/escaped-string/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-escaped-string.input.js diff --git a/codemods/create-element-to-jsx/tests/escaped-string/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-escaped-string.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/escaped-string/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-escaped-string.output.js diff --git a/codemods/create-element-to-jsx/tests/gt-lt-entities/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-gt-lt-entities.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/gt-lt-entities/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-gt-lt-entities.input.js diff --git a/codemods/create-element-to-jsx/tests/gt-lt-entities/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-gt-lt-entities.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/gt-lt-entities/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-gt-lt-entities.output.js diff --git a/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-ignore-bad-capitalization.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/ignore-bad-capitalization/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-ignore-bad-capitalization.input.js diff --git a/codemods/create-element-to-jsx/tests/ignore-bad-capitalization/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-ignore-bad-capitalization.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/ignore-bad-capitalization/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-ignore-bad-capitalization.output.js diff --git a/codemods/create-element-to-jsx/tests/literal-prop/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-literal-prop.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/literal-prop/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-literal-prop.input.js diff --git a/codemods/create-element-to-jsx/tests/literal-prop/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-literal-prop.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/literal-prop/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-literal-prop.output.js diff --git a/codemods/create-element-to-jsx/tests/literal-spacing/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-literal-spacing.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/literal-spacing/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-literal-spacing.input.js diff --git a/codemods/create-element-to-jsx/tests/literal-spacing/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-literal-spacing.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/literal-spacing/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-literal-spacing.output.js diff --git a/codemods/create-element-to-jsx/tests/member-expression-as-prop/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-member-expression-as-prop.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/member-expression-as-prop/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-member-expression-as-prop.input.js diff --git a/codemods/create-element-to-jsx/tests/member-expression-as-prop/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-member-expression-as-prop.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/member-expression-as-prop/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-member-expression-as-prop.output.js diff --git a/codemods/create-element-to-jsx/tests/no-props-arg/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-no-props-arg.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/no-props-arg/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-no-props-arg.input.js diff --git a/codemods/create-element-to-jsx/tests/no-props-arg/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-no-props-arg.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/no-props-arg/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-no-props-arg.output.js diff --git a/codemods/create-element-to-jsx/tests/no-react/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-no-react.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/no-react/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-no-react.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-no-react.output.js b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-no-react.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/create-element-to-jsx/tests/object-assign/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-object-assign.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/object-assign/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-object-assign.input.js diff --git a/codemods/create-element-to-jsx/tests/object-assign/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-object-assign.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/object-assign/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-object-assign.output.js diff --git a/codemods/create-element-to-jsx/tests/preserve-comments/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-preserve-comments.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/preserve-comments/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-preserve-comments.input.js diff --git a/codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-preserve-comments.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/preserve-comments/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-preserve-comments.output.js diff --git a/codemods/create-element-to-jsx/tests/props-array/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props-array.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/props-array/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props-array.input.js diff --git a/codemods/create-element-to-jsx/tests/props-array/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props-array.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/props-array/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props-array.output.js diff --git a/codemods/create-element-to-jsx/tests/props-boolean/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props-boolean.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/props-boolean/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props-boolean.input.js diff --git a/codemods/create-element-to-jsx/tests/props-boolean/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props-boolean.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/props-boolean/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props-boolean.output.js diff --git a/codemods/create-element-to-jsx/tests/props/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/props/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props.input.js diff --git a/codemods/create-element-to-jsx/tests/props/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/props/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-props.output.js diff --git a/codemods/create-element-to-jsx/tests/react-spread/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-react-spread.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/react-spread/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-react-spread.input.js diff --git a/codemods/create-element-to-jsx/tests/react-spread/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-react-spread.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/react-spread/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-react-spread.output.js diff --git a/codemods/create-element-to-jsx/tests/single-element/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-single-element.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/single-element/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-single-element.input.js diff --git a/codemods/create-element-to-jsx/tests/single-element/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-single-element.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/single-element/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-single-element.output.js diff --git a/codemods/create-element-to-jsx/tests/spread-props/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-spread-props.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/spread-props/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-spread-props.input.js diff --git a/codemods/create-element-to-jsx/tests/spread-props/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-spread-props.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/spread-props/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-spread-props.output.js diff --git a/codemods/create-element-to-jsx/tests/spread/input.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-spread.input.js similarity index 100% rename from codemods/create-element-to-jsx/tests/spread/input.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-spread.input.js diff --git a/codemods/create-element-to-jsx/tests/spread/expected.tsx b/codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-spread.output.js similarity index 100% rename from codemods/create-element-to-jsx/tests/spread/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/create-element-to-jsx-spread.output.js diff --git a/codemods/sort-comp/tests/custom-sort-group/.eslintrc b/codemods/legacy/transforms/__testfixtures__/custom-sort-group/.eslintrc similarity index 100% rename from codemods/sort-comp/tests/custom-sort-group/.eslintrc rename to codemods/legacy/transforms/__testfixtures__/custom-sort-group/.eslintrc diff --git a/codemods/sort-comp/tests/custom-sort-group/input.tsx b/codemods/legacy/transforms/__testfixtures__/custom-sort-group/custom-sort-group.input.js similarity index 100% rename from codemods/sort-comp/tests/custom-sort-group/input.tsx rename to codemods/legacy/transforms/__testfixtures__/custom-sort-group/custom-sort-group.input.js diff --git a/codemods/sort-comp/tests/custom-sort-group/expected.tsx b/codemods/legacy/transforms/__testfixtures__/custom-sort-group/custom-sort-group.output.js similarity index 100% rename from codemods/sort-comp/tests/custom-sort-group/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/custom-sort-group/custom-sort-group.output.js diff --git a/codemods/sort-comp/tests/custom-sort/.eslintrc b/codemods/legacy/transforms/__testfixtures__/custom-sort/.eslintrc similarity index 100% rename from codemods/sort-comp/tests/custom-sort/.eslintrc rename to codemods/legacy/transforms/__testfixtures__/custom-sort/.eslintrc diff --git a/codemods/sort-comp/tests/custom-sort/input.tsx b/codemods/legacy/transforms/__testfixtures__/custom-sort/custom-sort.input.js similarity index 100% rename from codemods/sort-comp/tests/custom-sort/input.tsx rename to codemods/legacy/transforms/__testfixtures__/custom-sort/custom-sort.input.js diff --git a/codemods/sort-comp/tests/custom-sort/expected.tsx b/codemods/legacy/transforms/__testfixtures__/custom-sort/custom-sort.output.js similarity index 100% rename from codemods/sort-comp/tests/custom-sort/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/custom-sort/custom-sort.output.js diff --git a/codemods/error-boundaries/tests/class-component/input.tsx b/codemods/legacy/transforms/__testfixtures__/error-boundaries/class-component.input.js similarity index 100% rename from codemods/error-boundaries/tests/class-component/input.tsx rename to codemods/legacy/transforms/__testfixtures__/error-boundaries/class-component.input.js diff --git a/codemods/error-boundaries/tests/class-component/expected.tsx b/codemods/legacy/transforms/__testfixtures__/error-boundaries/class-component.output.js similarity index 100% rename from codemods/error-boundaries/tests/class-component/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/error-boundaries/class-component.output.js diff --git a/codemods/error-boundaries/tests/create-class-component/input.tsx b/codemods/legacy/transforms/__testfixtures__/error-boundaries/create-class-component.input.js similarity index 100% rename from codemods/error-boundaries/tests/create-class-component/input.tsx rename to codemods/legacy/transforms/__testfixtures__/error-boundaries/create-class-component.input.js diff --git a/codemods/error-boundaries/tests/create-class-component/expected.tsx b/codemods/legacy/transforms/__testfixtures__/error-boundaries/create-class-component.output.js similarity index 100% rename from codemods/error-boundaries/tests/create-class-component/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/error-boundaries/create-class-component.output.js diff --git a/codemods/find-dom-node/tests/findDOMNode/input.tsx b/codemods/legacy/transforms/__testfixtures__/findDOMNode.input.js similarity index 100% rename from codemods/find-dom-node/tests/findDOMNode/input.tsx rename to codemods/legacy/transforms/__testfixtures__/findDOMNode.input.js diff --git a/codemods/find-dom-node/tests/findDOMNode/expected.tsx b/codemods/legacy/transforms/__testfixtures__/findDOMNode.output.js similarity index 100% rename from codemods/find-dom-node/tests/findDOMNode/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/findDOMNode.output.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/input.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow1.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/input.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow1.input.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow1.output.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow1.output.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/input.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow10.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/input.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow10.input.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow10.output.js similarity index 98% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow10.output.js index 1113e05..6da7e15 100644 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow10.output.js @@ -4,4 +4,4 @@ class Component extends React.Component { class Component2 extends React.Component { onClick = () => { }; -} \ No newline at end of file +} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow11.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow11.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow11.output.js b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow11.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/input.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow2.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/input.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow2.input.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow2.output.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow2.output.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow3.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow3.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow3.output.js b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow3.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/input.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow4.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/input.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow4.input.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow4.output.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow4.output.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/input.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow5.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/input.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow5.input.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow5.output.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow5.output.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/input.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow6.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/input.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow6.input.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow6.output.js similarity index 97% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow6.output.js index 3ce6360..2737b87 100644 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow6.output.js @@ -3,7 +3,7 @@ class Component extends React.Component { super(); const self: any = this; self._onMapResize = debounceCore(this._onMapResize.bind(this), 100); - } + } onClick = () => { }; onMapResize() { } } diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/input.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow7.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/input.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow7.input.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow7.output.js similarity index 95% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow7.output.js index 8f8e7fa..7c16f80 100644 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow7.output.js @@ -1,7 +1,7 @@ class SomeClass { constructor() { (this: any)._isLoaded = false; - } + } isLoaded = (): boolean => { return this._isLoaded; diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/input.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow8.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/input.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow8.input.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow8.output.js similarity index 60% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow8.output.js index 6f9abaf..a9d9934 100644 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow8.output.js @@ -1,3 +1,3 @@ class Component extends React.Component { - onClick = () => { }; + onClick = async () => { }; } diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/input.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow9.input.js similarity index 100% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/input.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow9.input.js diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/expected.tsx b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow9.output.js similarity index 98% rename from codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow9.output.js index 19c2ed0..7e258e4 100644 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/manual-bind-to-arrow/manual-bind-to-arrow9.output.js @@ -4,4 +4,4 @@ class Component extends React.Component { class Component2 extends React.Component { didClick = () => { }; -} \ No newline at end of file +} diff --git a/codemods/pure-component/tests/destructuring/input.tsx b/codemods/legacy/transforms/__testfixtures__/pure-component-destructuring.input.js similarity index 100% rename from codemods/pure-component/tests/destructuring/input.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-component-destructuring.input.js diff --git a/codemods/pure-component/tests/destructuring/expected.tsx b/codemods/legacy/transforms/__testfixtures__/pure-component-destructuring.output.js similarity index 100% rename from codemods/pure-component/tests/destructuring/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-component-destructuring.output.js diff --git a/codemods/pure-component/tests/default/input.tsx b/codemods/legacy/transforms/__testfixtures__/pure-component.input.js similarity index 100% rename from codemods/pure-component/tests/default/input.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-component.input.js diff --git a/codemods/pure-component/tests/default/expected.tsx b/codemods/legacy/transforms/__testfixtures__/pure-component.output.js similarity index 100% rename from codemods/pure-component/tests/default/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-component.output.js diff --git a/codemods/pure-component/tests/arrow/input.tsx b/codemods/legacy/transforms/__testfixtures__/pure-component2.input.js similarity index 100% rename from codemods/pure-component/tests/arrow/input.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-component2.input.js diff --git a/codemods/pure-component/tests/arrow/expected.tsx b/codemods/legacy/transforms/__testfixtures__/pure-component2.output.js similarity index 100% rename from codemods/pure-component/tests/arrow/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-component2.output.js diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin/input.tsx b/codemods/legacy/transforms/__testfixtures__/pure-render-mixin.input.js similarity index 100% rename from codemods/pure-render-mixin/tests/pure-render-mixin/input.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-render-mixin.input.js diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin/expected.tsx b/codemods/legacy/transforms/__testfixtures__/pure-render-mixin.output.js similarity index 100% rename from codemods/pure-render-mixin/tests/pure-render-mixin/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-render-mixin.output.js diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin2/input.tsx b/codemods/legacy/transforms/__testfixtures__/pure-render-mixin2.input.js similarity index 100% rename from codemods/pure-render-mixin/tests/pure-render-mixin2/input.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-render-mixin2.input.js diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin2/expected.tsx b/codemods/legacy/transforms/__testfixtures__/pure-render-mixin2.output.js similarity index 100% rename from codemods/pure-render-mixin/tests/pure-render-mixin2/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-render-mixin2.output.js diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin3/input.tsx b/codemods/legacy/transforms/__testfixtures__/pure-render-mixin3.input.js similarity index 100% rename from codemods/pure-render-mixin/tests/pure-render-mixin3/input.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-render-mixin3.input.js diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin3/expected.tsx b/codemods/legacy/transforms/__testfixtures__/pure-render-mixin3.output.js similarity index 100% rename from codemods/pure-render-mixin/tests/pure-render-mixin3/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-render-mixin3.output.js diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin4/input.tsx b/codemods/legacy/transforms/__testfixtures__/pure-render-mixin4.input.js similarity index 100% rename from codemods/pure-render-mixin/tests/pure-render-mixin4/input.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-render-mixin4.input.js diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin4/expected.tsx b/codemods/legacy/transforms/__testfixtures__/pure-render-mixin4.output.js similarity index 100% rename from codemods/pure-render-mixin/tests/pure-render-mixin4/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/pure-render-mixin4.output.js diff --git a/codemods/react-to-react-dom/tests/import-dom-base/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-dom-base.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-dom-base/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-dom-base.input.js diff --git a/codemods/react-to-react-dom/tests/import-dom-base/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-dom-base.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-dom-base/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-dom-base.output.js diff --git a/codemods/react-to-react-dom/tests/import-multiple-specifiers/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-multiple-specifiers.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-multiple-specifiers/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-multiple-specifiers.input.js diff --git a/codemods/react-to-react-dom/tests/import-multiple-specifiers/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-multiple-specifiers.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-multiple-specifiers/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-multiple-specifiers.output.js diff --git a/codemods/react-to-react-dom/tests/import-server-base/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-server-base.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-server-base/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-server-base.input.js diff --git a/codemods/react-to-react-dom/tests/import-server-base/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-server-base.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-server-base/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-server-base.output.js diff --git a/codemods/react-to-react-dom/tests/import-with-existing-react-dom/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-with-existing-react-dom.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-with-existing-react-dom/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-with-existing-react-dom.input.js diff --git a/codemods/react-to-react-dom/tests/import-with-existing-react-dom/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-with-existing-react-dom.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-with-existing-react-dom/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-with-existing-react-dom.output.js diff --git a/codemods/react-to-react-dom/tests/import-without-default-specifier/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-without-default-specifier.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-without-default-specifier/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-without-default-specifier.input.js diff --git a/codemods/react-to-react-dom/tests/import-without-default-specifier/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-without-default-specifier.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/import-without-default-specifier/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/import-without-default-specifier.output.js diff --git a/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/mixed-with-existing-react-dom.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/mixed-with-existing-react-dom.input.js diff --git a/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/mixed-with-existing-react-dom.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/mixed-with-existing-react-dom.output.js diff --git a/codemods/react-to-react-dom/tests/require-dom-base/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-dom-base.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/require-dom-base/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-dom-base.input.js diff --git a/codemods/react-to-react-dom/tests/require-dom-base/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-dom-base.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/require-dom-base/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-dom-base.output.js diff --git a/codemods/react-to-react-dom/tests/require-indirect/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-indirect.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/require-indirect/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-indirect.input.js diff --git a/codemods/react-to-react-dom/tests/require-indirect/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-indirect.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/require-indirect/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-indirect.output.js diff --git a/codemods/react-to-react-dom/tests/require-keeps-react/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-keeps-react.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/require-keeps-react/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-keeps-react.input.js diff --git a/codemods/react-to-react-dom/tests/require-keeps-react/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-keeps-react.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/require-keeps-react/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-keeps-react.output.js diff --git a/codemods/react-to-react-dom/tests/require-server-base/input.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-server-base.input.js similarity index 100% rename from codemods/react-to-react-dom/tests/require-server-base/input.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-server-base.input.js diff --git a/codemods/react-to-react-dom/tests/require-server-base/expected.tsx b/codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-server-base.output.js similarity index 100% rename from codemods/react-to-react-dom/tests/require-server-base/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/react-to-react-dom/require-server-base.output.js diff --git a/codemods/remove-context-provider/tests/no-provider/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/no-provider.input.js similarity index 100% rename from codemods/remove-context-provider/tests/no-provider/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/no-provider.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/remove-context-provider/no-provider.output.js b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/no-provider.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/remove-context-provider/tests/typescript-no-provider/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/no-provider.input.js similarity index 100% rename from codemods/remove-context-provider/tests/typescript-no-provider/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/no-provider.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/no-provider.output.js b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/no-provider.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/remove-context-provider/tests/typescript-with-provider-2/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider-2.input.js similarity index 100% rename from codemods/remove-context-provider/tests/typescript-with-provider-2/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider-2.input.js diff --git a/codemods/remove-context-provider/tests/typescript-no-provider/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider-2.output.js similarity index 100% rename from codemods/remove-context-provider/tests/typescript-no-provider/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider-2.output.js diff --git a/codemods/remove-context-provider/tests/typescript-with-provider/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider.input.js similarity index 100% rename from codemods/remove-context-provider/tests/typescript-with-provider/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider.input.js diff --git a/codemods/remove-context-provider/tests/typescript-with-provider/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider.output.js similarity index 100% rename from codemods/remove-context-provider/tests/typescript-with-provider/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/typescript/with-provider.output.js diff --git a/codemods/remove-context-provider/tests/with-provider-2/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/with-provider-2.input.js similarity index 100% rename from codemods/remove-context-provider/tests/with-provider-2/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/with-provider-2.input.js diff --git a/codemods/remove-context-provider/tests/no-provider/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/with-provider-2.output.js similarity index 100% rename from codemods/remove-context-provider/tests/no-provider/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/with-provider-2.output.js diff --git a/codemods/remove-context-provider/tests/with-provider/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/with-provider.input.js similarity index 100% rename from codemods/remove-context-provider/tests/with-provider/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/with-provider.input.js diff --git a/codemods/remove-context-provider/tests/with-provider/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-context-provider/with-provider.output.js similarity index 100% rename from codemods/remove-context-provider/tests/with-provider/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-context-provider/with-provider.output.js diff --git a/codemods/remove-forward-ref/tests/arrow-function-expression/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/arrow-function-expression.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/arrow-function-expression/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/arrow-function-expression.input.js diff --git a/codemods/remove-forward-ref/tests/arrow-function-expression/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/arrow-function-expression.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/arrow-function-expression/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/arrow-function-expression.output.js diff --git a/codemods/remove-forward-ref/tests/callee-is-member-expression/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/callee-is-member-expression.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/callee-is-member-expression/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/callee-is-member-expression.input.js diff --git a/codemods/remove-forward-ref/tests/callee-is-member-expression/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/callee-is-member-expression.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/callee-is-member-expression/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/callee-is-member-expression.output.js diff --git a/codemods/remove-forward-ref/tests/forward-ref-import-2/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import-2.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/forward-ref-import-2/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import-2.input.js diff --git a/codemods/remove-forward-ref/tests/forward-ref-import-2/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import-2.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/forward-ref-import-2/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import-2.output.js diff --git a/codemods/remove-forward-ref/tests/forward-ref-import/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/forward-ref-import/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import.input.js diff --git a/codemods/remove-forward-ref/tests/forward-ref-import/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/forward-ref-import/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/forward-ref-import.output.js diff --git a/codemods/remove-forward-ref/tests/function-expression/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/function-expression.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/function-expression/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/function-expression.input.js diff --git a/codemods/remove-forward-ref/tests/function-expression/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/function-expression.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/function-expression/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/function-expression.output.js diff --git a/codemods/remove-forward-ref/tests/props-identifier/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/props-identifier.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/props-identifier/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/props-identifier.input.js diff --git a/codemods/remove-forward-ref/tests/props-identifier/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/props-identifier.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/props-identifier/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/props-identifier.output.js diff --git a/codemods/remove-forward-ref/tests/props-object-pattern/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/props-object-pattern.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/props-object-pattern/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/props-object-pattern.input.js diff --git a/codemods/remove-forward-ref/tests/props-object-pattern/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/props-object-pattern.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/props-object-pattern/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/props-object-pattern.output.js diff --git a/codemods/remove-forward-ref/tests/typescript-props-type-literal/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/typescript-props-type-literal/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.input.js diff --git a/codemods/remove-forward-ref/tests/typescript-props-type-literal/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/typescript-props-type-literal/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.output.js diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.input.js diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.output.js diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.input.js diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.output.js diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments/input.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.input.js similarity index 100% rename from codemods/remove-forward-ref/tests/typescript-type-arguments/input.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.input.js diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments/expected.tsx b/codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.output.js similarity index 100% rename from codemods/remove-forward-ref/tests/typescript-type-arguments/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.output.js diff --git a/codemods/rename-unsafe-lifecycles/tests/arrow-functions/input.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/arrow-functions.input.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/arrow-functions/input.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/arrow-functions.input.js diff --git a/codemods/rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/arrow-functions.output.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/arrow-functions/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/arrow-functions.output.js diff --git a/codemods/rename-unsafe-lifecycles/tests/create-react-class/input.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/create-react-class.input.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/create-react-class/input.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/create-react-class.input.js diff --git a/codemods/rename-unsafe-lifecycles/tests/create-react-class/expected.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/create-react-class.output.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/create-react-class/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/create-react-class.output.js diff --git a/codemods/rename-unsafe-lifecycles/tests/instance-methods/input.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/instance-methods.input.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/instance-methods/input.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/instance-methods.input.js diff --git a/codemods/rename-unsafe-lifecycles/tests/instance-methods/expected.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/instance-methods.output.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/instance-methods/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/instance-methods.output.js diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/input.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-calling-lifecycles.input.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/input.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-calling-lifecycles.input.js diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/expected.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-calling-lifecycles.output.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-calling-lifecycles.output.js diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/input.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-invoked-mixin-methods.input.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/input.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-invoked-mixin-methods.input.js diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/expected.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-invoked-mixin-methods.output.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/manually-invoked-mixin-methods.output.js diff --git a/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/input.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/one-lifecycle-calls-another.input.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/input.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/one-lifecycle-calls-another.input.js diff --git a/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/expected.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/one-lifecycle-calls-another.output.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/one-lifecycle-calls-another.output.js diff --git a/codemods/rename-unsafe-lifecycles/tests/standalone-function/expected.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/standalone-function.input.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/standalone-function/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/standalone-function.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/standalone-function.output.js b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/standalone-function.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/typescript/class.tsx.input.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/typescript/class.tsx.input.js diff --git a/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/typescript/class.tsx.output.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/typescript/class.tsx.output.js diff --git a/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/expected.tsx b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/variable-within-class-method.input.js similarity index 100% rename from codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/variable-within-class-method.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/variable-within-class-method.output.js b/codemods/legacy/transforms/__testfixtures__/rename-unsafe-lifecycles/variable-within-class-method.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/sort-comp/tests/sort-comp-pure/input.tsx b/codemods/legacy/transforms/__testfixtures__/sort-comp-pure.input.js similarity index 100% rename from codemods/sort-comp/tests/sort-comp-pure/input.tsx rename to codemods/legacy/transforms/__testfixtures__/sort-comp-pure.input.js diff --git a/codemods/sort-comp/tests/sort-comp-pure/expected.tsx b/codemods/legacy/transforms/__testfixtures__/sort-comp-pure.output.js similarity index 100% rename from codemods/sort-comp/tests/sort-comp-pure/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/sort-comp-pure.output.js diff --git a/codemods/sort-comp/tests/sort-comp/input.tsx b/codemods/legacy/transforms/__testfixtures__/sort-comp.input.js similarity index 100% rename from codemods/sort-comp/tests/sort-comp/input.tsx rename to codemods/legacy/transforms/__testfixtures__/sort-comp.input.js diff --git a/codemods/sort-comp/tests/sort-comp/expected.tsx b/codemods/legacy/transforms/__testfixtures__/sort-comp.output.js similarity index 100% rename from codemods/sort-comp/tests/sort-comp/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/sort-comp.output.js diff --git a/codemods/sort-comp/tests/sort-comp2/input.tsx b/codemods/legacy/transforms/__testfixtures__/sort-comp2.input.js similarity index 100% rename from codemods/sort-comp/tests/sort-comp2/input.tsx rename to codemods/legacy/transforms/__testfixtures__/sort-comp2.input.js diff --git a/codemods/sort-comp/tests/sort-comp2/expected.tsx b/codemods/legacy/transforms/__testfixtures__/sort-comp2.output.js similarity index 100% rename from codemods/sort-comp/tests/sort-comp2/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/sort-comp2.output.js diff --git a/codemods/sort-comp/tests/sort-comp3/input.tsx b/codemods/legacy/transforms/__testfixtures__/sort-comp3.input.js similarity index 100% rename from codemods/sort-comp/tests/sort-comp3/input.tsx rename to codemods/legacy/transforms/__testfixtures__/sort-comp3.input.js diff --git a/codemods/sort-comp/tests/sort-comp3/expected.tsx b/codemods/legacy/transforms/__testfixtures__/sort-comp3.output.js similarity index 100% rename from codemods/sort-comp/tests/sort-comp3/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/sort-comp3.output.js diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import-react-variable.input.js similarity index 100% rename from codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import-react-variable.input.js diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import-react-variable.output.js similarity index 99% rename from codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import-react-variable.output.js index 0302b77..41bcef4 100644 --- a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import-react-variable.output.js @@ -1,5 +1,6 @@ import { createElement, useState } from "react"; import * as React from "react"; + React.createElement('div', {});
    Hi
    ; diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import.input.js similarity index 100% rename from codemods/update-react-imports/tests/default-and-multiple-specifiers-import/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import.input.js diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import.output.js similarity index 99% rename from codemods/update-react-imports/tests/default-and-multiple-specifiers-import/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import.output.js index 83cca58..ded95b7 100644 --- a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/default-and-multiple-specifiers-import.output.js @@ -1,3 +1,4 @@ import type { Element } from "react"; import { createElement, useState } from "react"; +
    Hi
    ; diff --git a/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-react-not-removed.input.js similarity index 100% rename from codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-react-not-removed.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-react-not-removed.output.js b/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-react-not-removed.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-variable-used.input.js similarity index 100% rename from codemods/update-react-imports/tests/destructure-named-imports-variable-used/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-variable-used.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-variable-used.output.js b/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports-variable-used.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/update-react-imports/tests/destructure-named-imports/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports.input.js similarity index 100% rename from codemods/update-react-imports/tests/destructure-named-imports/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports.output.js b/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports.output.js new file mode 100644 index 0000000..2ebfe7c --- /dev/null +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/destructure-named-imports.output.js @@ -0,0 +1,5 @@ +import { useState } from "react"; + +useState(false); + +
    \ No newline at end of file diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import-react-variable.input.js similarity index 100% rename from codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import-react-variable.input.js diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import-react-variable.output.js similarity index 99% rename from codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import-react-variable.output.js index 827ccc9..fd12c75 100644 --- a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import-react-variable.output.js @@ -1,5 +1,6 @@ import type { Element, Component } from "react"; import { createElement } from "react"; + createElement('div', {});
    Hi
    ; diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import.input.js similarity index 100% rename from codemods/update-react-imports/tests/flow-default-and-type-specifier-import/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import.input.js diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import.output.js similarity index 98% rename from codemods/update-react-imports/tests/flow-default-and-type-specifier-import/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import.output.js index 5d45ddc..1a4260e 100644 --- a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/flow-default-and-type-specifier-import.output.js @@ -1,2 +1,3 @@ import type { Element } from "react"; +
    Hi
    ; diff --git a/codemods/update-react-imports/tests/jsx-element/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/jsx-element.input.js similarity index 100% rename from codemods/update-react-imports/tests/jsx-element/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/jsx-element.input.js diff --git a/codemods/update-react-imports/tests/jsx-element/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/jsx-element.output.js similarity index 100% rename from codemods/update-react-imports/tests/jsx-element/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/jsx-element.output.js diff --git a/codemods/update-react-imports/tests/jsx-fragment/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/jsx-fragment.input.js similarity index 100% rename from codemods/update-react-imports/tests/jsx-fragment/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/jsx-fragment.input.js diff --git a/codemods/update-react-imports/tests/jsx-fragment/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/jsx-fragment.output.js similarity index 100% rename from codemods/update-react-imports/tests/jsx-fragment/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/jsx-fragment.output.js diff --git a/codemods/update-react-imports/tests/leading-comment/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/leading-comment.input.js similarity index 100% rename from codemods/update-react-imports/tests/leading-comment/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/leading-comment.input.js diff --git a/codemods/update-react-imports/tests/leading-comment/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/leading-comment.output.js similarity index 100% rename from codemods/update-react-imports/tests/leading-comment/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/leading-comment.output.js diff --git a/codemods/update-react-imports/tests/react-already-used-named-export/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-already-used-named-export.input.js similarity index 100% rename from codemods/update-react-imports/tests/react-already-used-named-export/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-already-used-named-export.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-already-used-named-export.output.js b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-already-used-named-export.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element-react-variable.input.js similarity index 100% rename from codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element-react-variable.input.js diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element-react-variable.output.js similarity index 98% rename from codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element-react-variable.output.js index 4a3728b..3995e26 100644 --- a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element-react-variable.output.js @@ -1,4 +1,5 @@ import { createElement } from "react"; + createElement('div', {});
    ; diff --git a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element.input.js similarity index 100% rename from codemods/update-react-imports/tests/react-basic-default-export-jsx-element/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element.input.js diff --git a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element.output.js similarity index 100% rename from codemods/update-react-imports/tests/react-basic-default-export-jsx-element/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export-jsx-element.output.js diff --git a/codemods/update-react-imports/tests/react-basic-default-export/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export.input.js similarity index 100% rename from codemods/update-react-imports/tests/react-basic-default-export/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export.input.js diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export.output.js similarity index 98% rename from codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export.output.js index bf4a35e..c4c96c0 100644 --- a/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-basic-default-export.output.js @@ -1,2 +1,3 @@ import { createElement } from "react"; + createElement('div', 'la'); diff --git a/codemods/update-react-imports/tests/react-jsx-member-expression/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-jsx-member-expression.input.js similarity index 100% rename from codemods/update-react-imports/tests/react-jsx-member-expression/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-jsx-member-expression.input.js diff --git a/codemods/update-react-imports/tests/react-jsx-member-expression/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-jsx-member-expression.output.js similarity index 75% rename from codemods/update-react-imports/tests/react-jsx-member-expression/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-jsx-member-expression.output.js index dd7360f..7627281 100644 --- a/codemods/update-react-imports/tests/react-jsx-member-expression/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-jsx-member-expression.output.js @@ -1,2 +1,3 @@ import { Fragment, useState } from 'react'; - \ No newline at end of file + + diff --git a/codemods/update-react-imports/tests/react-not-removed/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-not-removed.input.js similarity index 100% rename from codemods/update-react-imports/tests/react-not-removed/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-not-removed.input.js diff --git a/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-not-removed.output.js similarity index 99% rename from codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-not-removed.output.js index 9f5bcfc..8dc1bf7 100644 --- a/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-not-removed.output.js @@ -1,4 +1,5 @@ import * as React from "react"; + React.createElement('div', {}); Promise.resolve(React); diff --git a/codemods/update-react-imports/tests/react-type-default-export/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-default-export.input.js similarity index 100% rename from codemods/update-react-imports/tests/react-type-default-export/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-default-export.input.js diff --git a/codemods/update-react-imports/tests/react-type-default-export/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-default-export.output.js similarity index 97% rename from codemods/update-react-imports/tests/react-type-default-export/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-default-export.output.js index 36e2cbd..441a929 100644 --- a/codemods/update-react-imports/tests/react-type-default-export/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-default-export.output.js @@ -1,2 +1,3 @@ import type React from "react"; +
    Hi
    ; diff --git a/codemods/update-react-imports/tests/react-type-not-removed/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-not-removed.input.js similarity index 100% rename from codemods/update-react-imports/tests/react-type-not-removed/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-not-removed.input.js diff --git a/codemods/update-react-imports/tests/react-type-not-removed/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-not-removed.output.js similarity index 98% rename from codemods/update-react-imports/tests/react-type-not-removed/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-not-removed.output.js index 0dcfc2b..11e80fe 100644 --- a/codemods/update-react-imports/tests/react-type-not-removed/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/react-type-not-removed.output.js @@ -1,2 +1,3 @@ import type React, { Node } from "react"; +
    Hi
    ; diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import-react-variable.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import-react-variable.tsx.input.js diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import-react-variable.tsx.output.js similarity index 99% rename from codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import-react-variable.tsx.output.js index 0302b77..41bcef4 100644 --- a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import-react-variable.tsx.output.js @@ -1,5 +1,6 @@ import { createElement, useState } from "react"; import * as React from "react"; + React.createElement('div', {});
    Hi
    ; diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import.tsx.input.js diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import.tsx.output.js similarity index 98% rename from codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import.tsx.output.js index d617eef..51ebe95 100644 --- a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/default-and-multiple-specifiers-import.tsx.output.js @@ -1,2 +1,3 @@ import { createElement, useState } from "react"; +
    Hi
    ; diff --git a/codemods/update-react-imports/tests/typescript-jsx-element-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-element.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-jsx-element-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-element.tsx.input.js diff --git a/codemods/update-react-imports/tests/typescript-jsx-element-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-element.tsx.output.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-jsx-element-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-element.tsx.output.js diff --git a/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-fragment.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-fragment.tsx.input.js diff --git a/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-fragment.tsx.output.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/jsx-fragment.tsx.output.js diff --git a/codemods/update-react-imports/tests/typescript-leading-comment-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/leading-comment.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-leading-comment-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/leading-comment.tsx.input.js diff --git a/codemods/update-react-imports/tests/typescript-leading-comment-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/leading-comment.tsx.output.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-leading-comment-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/leading-comment.tsx.output.js diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-default.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-default.tsx.input.js diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-default.tsx.output.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-default.tsx.output.js diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-namespace.tsx.input.js similarity index 99% rename from codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-namespace.tsx.input.js index f9f64e1..f27298b 100644 --- a/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-namespace.tsx.input.js @@ -1,4 +1,5 @@ import * as React from 'react'; + const App: React.FunctionComponent<{ message: string }> = ({ message }) => (
    {message}
    ); diff --git a/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-namespace.tsx.output.js b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/preserve-types-namespace.tsx.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-already-used-named-export.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-already-used-named-export.tsx.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-already-used-named-export.tsx.output.js b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-already-used-named-export.tsx.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element-react-variable.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element-react-variable.tsx.input.js diff --git a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element-react-variable.tsx.output.js similarity index 98% rename from codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element-react-variable.tsx.output.js index 4a3728b..3995e26 100644 --- a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element-react-variable.tsx.output.js @@ -1,4 +1,5 @@ import { createElement } from "react"; + createElement('div', {});
    ; diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element.tsx.input.js diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element.tsx.output.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export-jsx-element.tsx.output.js diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export.tsx.input.js diff --git a/codemods/update-react-imports/tests/react-basic-default-export/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export.tsx.output.js similarity index 98% rename from codemods/update-react-imports/tests/react-basic-default-export/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export.tsx.output.js index bf4a35e..c4c96c0 100644 --- a/codemods/update-react-imports/tests/react-basic-default-export/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-basic-default-export.tsx.output.js @@ -1,2 +1,3 @@ import { createElement } from "react"; + createElement('div', 'la'); diff --git a/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-jsx-member-expression.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-jsx-member-expression.tsx.input.js diff --git a/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-jsx-member-expression.tsx.output.js similarity index 75% rename from codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-jsx-member-expression.tsx.output.js index dd7360f..7627281 100644 --- a/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-jsx-member-expression.tsx.output.js @@ -1,2 +1,3 @@ import { Fragment, useState } from 'react'; - \ No newline at end of file + + diff --git a/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-not-removed.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-react-not-removed-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-not-removed.tsx.input.js diff --git a/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/expected.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-not-removed.tsx.output.js similarity index 85% rename from codemods/update-react-imports/tests/typescript-react-not-removed-tsx/expected.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-not-removed.tsx.output.js index 4b9bcea..8dc1bf7 100644 --- a/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/expected.tsx +++ b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/react-not-removed.tsx.output.js @@ -1,6 +1,7 @@ import * as React from "react"; + React.createElement('div', {}); Promise.resolve(React); -
    Hi
    \ No newline at end of file +
    Hi
    diff --git a/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/variable-already-used.tsx.input.js similarity index 100% rename from codemods/update-react-imports/tests/typescript-variable-already-used-tsx/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/variable-already-used.tsx.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/variable-already-used.tsx.output.js b/codemods/legacy/transforms/__testfixtures__/update-react-imports/typescript/variable-already-used.tsx.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/update-react-imports/tests/variable-already-used/input.tsx b/codemods/legacy/transforms/__testfixtures__/update-react-imports/variable-already-used.input.js similarity index 100% rename from codemods/update-react-imports/tests/variable-already-used/input.tsx rename to codemods/legacy/transforms/__testfixtures__/update-react-imports/variable-already-used.input.js diff --git a/codemods/legacy/transforms/__testfixtures__/update-react-imports/variable-already-used.output.js b/codemods/legacy/transforms/__testfixtures__/update-react-imports/variable-already-used.output.js new file mode 100644 index 0000000..e69de29 diff --git a/codemods/legacy/transforms/__tests__/React-DOM-to-react-dom-factories-test.js b/codemods/legacy/transforms/__tests__/React-DOM-to-react-dom-factories-test.js new file mode 100644 index 0000000..bf9fbaa --- /dev/null +++ b/codemods/legacy/transforms/__tests__/React-DOM-to-react-dom-factories-test.js @@ -0,0 +1,34 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const tests = [ + 'react-dom-basic-case', + 'react-dom-deconstructed-import', + 'react-dom-deconstructed-require', + 'react-dom-deconstructed-require-part-two', + 'react-dom-no-change-import', + 'react-dom-no-change-require', + 'react-dom-no-change-import-dom-from-other-libraries', + 'react-dom-no-change-require-dom-from-other-libraries', + 'react-dom-no-change-local-dom-from-other-libraries' +]; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +describe('React-DOM-to-react-dom-factories', () => { + tests.forEach(test => + defineTest( + __dirname, + 'React-DOM-to-react-dom-factories', + null, + `React-DOM-to-react-dom-factories/${ test }` + ) + ); +}); diff --git a/codemods/legacy/transforms/__tests__/ReactNative-View-propTypes-test.js b/codemods/legacy/transforms/__tests__/ReactNative-View-propTypes-test.js new file mode 100644 index 0000000..d1e13f8 --- /dev/null +++ b/codemods/legacy/transforms/__tests__/ReactNative-View-propTypes-test.js @@ -0,0 +1,43 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +jest.mock('../ReactNative-View-propTypes', () => { + return Object.assign(jest.requireActual('../ReactNative-View-propTypes'), { + parser: 'flow' + }); +}); + +const tests = [ + 'default-import-multi-reference', + 'default-import-only-reference', + 'default-require-multi-reference', + 'default-require-only-reference', + 'destructured-import-multi-reference', + 'destructured-import-only-reference', + 'destructured-require-multi-reference', + 'destructured-require-only-reference', + 'import-flow-type-with-require', + 'multiple-replacements', + 'noop-import', + 'noop-require', +]; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +describe('ReactNative-View-propTypes', () => { + tests.forEach(test => + defineTest( + __dirname, + 'ReactNative-View-propTypes', + null, + `ReactNative-View-propTypes/${ test }` + ) + ); +}); diff --git a/codemods/legacy/transforms/__tests__/class-test.js b/codemods/legacy/transforms/__tests__/class-test.js new file mode 100644 index 0000000..e46306c --- /dev/null +++ b/codemods/legacy/transforms/__tests__/class-test.js @@ -0,0 +1,68 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +const pureMixinAlternativeOption = { + 'mixin-module-name': 'ReactComponentWithPureRenderMixin', + 'pure-component': true, +}; + +const enableFlowOption = {flow: true}; + +defineTest(__dirname, 'class', null, 'class/class'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-anonymous'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-anonymous2'); +defineTest(__dirname, 'class', pureMixinAlternativeOption, 'class/class-test2'); +defineTest(__dirname, 'class', enableFlowOption, 'class/export-default-class'); +defineTest(__dirname, 'class', pureMixinAlternativeOption, 'class/class-pure-mixin1'); +defineTest(__dirname, 'class', { + ...enableFlowOption, + 'pure-component': true, +}, 'class/class-pure-mixin2'); +defineTest(__dirname, 'class', null, 'class/class-pure-mixin3'); +defineTest(__dirname, 'class', { + ...pureMixinAlternativeOption, + ...enableFlowOption, +}, 'class/class-pure-mixin4'); +defineTest(__dirname, 'class', { + ...pureMixinAlternativeOption, +}, 'class/class-pure-mixin5'); +defineTest(__dirname, 'class', { + ...pureMixinAlternativeOption, + ...enableFlowOption, +}, 'class/class-top-comment'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-initial-state'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-property-field'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-flow1'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-flow2'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-flow3'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-flow4'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-flow5'); +defineTest(__dirname, 'class', enableFlowOption, 'class/class-flow6'); +defineTest(__dirname, 'class', { + ...enableFlowOption, + 'remove-runtime-proptypes': true, +}, 'class/class-flow7'); +defineTest(__dirname, 'class', null, 'class/class-prune-react'); +defineTest(__dirname, 'class', null, 'class/class-prune-react2'); +defineTest(__dirname, 'class', null, 'class/class-prune-react3'); +defineTest(__dirname, 'class', null, 'class/class-prune-react4'); +defineTest(__dirname, 'class', { + 'create-class-module-name': 'createReactClass__deprecated', + 'create-class-variable-name': 'createReactClass__deprecated', +}, 'class/class-create-class-naming'); +defineTest(__dirname, 'class', null, 'class/class-displayName'); +defineTest(__dirname, 'class', { + 'conversion': false, +}, 'class/class-no-conversion'); +defineTest(__dirname, 'class', { + 'display-name': false, +}, 'class/class-no-display-name'); diff --git a/codemods/legacy/transforms/__tests__/create-element-to-jsx-test.js b/codemods/legacy/transforms/__tests__/create-element-to-jsx-test.js new file mode 100644 index 0000000..b0739c5 --- /dev/null +++ b/codemods/legacy/transforms/__tests__/create-element-to-jsx-test.js @@ -0,0 +1,199 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +jest.mock('../create-element-to-jsx', () => { + return Object.assign(jest.requireActual('../create-element-to-jsx'), { + parser: 'flow' + }); +}); + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; +describe('create-element-to-jsx', () => { + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-single-element' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-props' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-props-boolean' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-props-array' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-children-literal' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-children' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-children-map' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-children-mixed-empty-string' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-spread' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-spread-props' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-no-react' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-literal-prop' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-call-as-children' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-react-spread' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-object-assign' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-member-expression-as-prop' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-call-expression-as-prop' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-allow-member-expression' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-gt-lt-entities' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-escaped-string' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-no-props-arg' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-preserve-comments' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-ignore-bad-capitalization' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-arg-spread' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-computed-component' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-deep-nesting' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-literal-spacing' + ); + defineTest( + __dirname, + 'create-element-to-jsx', + null, + 'create-element-to-jsx-element-comment-positioning' + ); + + it('throws when it does not recognize a property type', () => { + const jscodeshift = require('jscodeshift'); + const transform = require('../../transforms/create-element-to-jsx'); + const source = ` + var React = require("react/addons"); + React.createElement("foo", 1) + `; + + expect(() => transform({source}, {jscodeshift}, {})) + .toThrowError('Unexpected attribute of type "Literal"'); + }); +}); diff --git a/codemods/legacy/transforms/__tests__/custom-sort-group.js b/codemods/legacy/transforms/__tests__/custom-sort-group.js new file mode 100644 index 0000000..13ee78c --- /dev/null +++ b/codemods/legacy/transforms/__tests__/custom-sort-group.js @@ -0,0 +1,14 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +// The test fixtures for this are in their own dir so it can customize eslint with method grouping. +defineTest(__dirname, 'sort-comp', null, 'custom-sort-group/custom-sort-group'); diff --git a/codemods/legacy/transforms/__tests__/custom-sort.js b/codemods/legacy/transforms/__tests__/custom-sort.js new file mode 100644 index 0000000..0dfbfbe --- /dev/null +++ b/codemods/legacy/transforms/__tests__/custom-sort.js @@ -0,0 +1,14 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +// The test fixtures for this are in their own dir so it can customize eslint. +defineTest(__dirname, 'sort-comp', null, 'custom-sort/custom-sort'); diff --git a/codemods/legacy/transforms/__tests__/error-boundaries.js b/codemods/legacy/transforms/__tests__/error-boundaries.js new file mode 100644 index 0000000..8ed1c96 --- /dev/null +++ b/codemods/legacy/transforms/__tests__/error-boundaries.js @@ -0,0 +1,17 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const tests = ['class-component', 'create-class-component']; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +tests.forEach(test => { + defineTest(__dirname, 'error-boundaries', null, `error-boundaries/${test}`); +}); diff --git a/codemods/legacy/transforms/__tests__/findDOMNode-test.js b/codemods/legacy/transforms/__tests__/findDOMNode-test.js new file mode 100644 index 0000000..16f87eb --- /dev/null +++ b/codemods/legacy/transforms/__tests__/findDOMNode-test.js @@ -0,0 +1,12 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; +defineTest(__dirname, 'findDOMNode'); diff --git a/codemods/legacy/transforms/__tests__/manual-bind-to-arrow-test.js b/codemods/legacy/transforms/__tests__/manual-bind-to-arrow-test.js new file mode 100644 index 0000000..30e0827 --- /dev/null +++ b/codemods/legacy/transforms/__tests__/manual-bind-to-arrow-test.js @@ -0,0 +1,40 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +jest.mock('../manual-bind-to-arrow', () => { + return Object.assign(jest.requireActual('../manual-bind-to-arrow'), { + parser: 'flow' + }); +}); + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +var TESTS = [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11 +]; + +TESTS.forEach(test => { + defineTest( + __dirname, + 'manual-bind-to-arrow', + {flow: true}, + 'manual-bind-to-arrow/manual-bind-to-arrow' + test + ); +}); diff --git a/codemods/legacy/transforms/__tests__/pure-component-test.js b/codemods/legacy/transforms/__tests__/pure-component-test.js new file mode 100644 index 0000000..0526d5a --- /dev/null +++ b/codemods/legacy/transforms/__tests__/pure-component-test.js @@ -0,0 +1,20 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +jest.mock('../pure-component', () => { + return Object.assign(jest.requireActual('../pure-component'), { + parser: 'flow' + }); +}); + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; +defineTest(__dirname, 'pure-component'); +defineTest(__dirname, 'pure-component', { useArrows: true }, 'pure-component2'); +defineTest(__dirname, 'pure-component', { destructuring: true }, 'pure-component-destructuring'); diff --git a/codemods/legacy/transforms/__tests__/pure-render-mixin-test.js b/codemods/legacy/transforms/__tests__/pure-render-mixin-test.js new file mode 100644 index 0000000..776cf67 --- /dev/null +++ b/codemods/legacy/transforms/__tests__/pure-render-mixin-test.js @@ -0,0 +1,20 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; +defineTest(__dirname, 'pure-render-mixin'); +defineTest(__dirname, 'pure-render-mixin', null, 'pure-render-mixin2'); +defineTest(__dirname, 'pure-render-mixin', null, 'pure-render-mixin3'); +defineTest( + __dirname, + 'pure-render-mixin', + {'mixin-name': 'ReactComponentWithPureRenderMixin'}, + 'pure-render-mixin4' +); diff --git a/codemods/legacy/transforms/__tests__/react-to-react-dom-test.js b/codemods/legacy/transforms/__tests__/react-to-react-dom-test.js new file mode 100644 index 0000000..b00f92c --- /dev/null +++ b/codemods/legacy/transforms/__tests__/react-to-react-dom-test.js @@ -0,0 +1,34 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const tests = [ + 'require-dom-base', + 'require-server-base', + 'require-keeps-react', + 'require-indirect', + 'import-dom-base', + 'import-server-base', + 'import-multiple-specifiers', + 'mixed-with-existing-react-dom', + 'import-with-existing-react-dom', + 'import-without-default-specifier', +]; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; +describe('react-to-react-dom', () => { + tests.forEach(test => + defineTest( + __dirname, + 'react-to-react-dom', + null, + `react-to-react-dom/${ test }` + ) + ); +}); diff --git a/codemods/legacy/transforms/__tests__/remove-context-provider.test.ts b/codemods/legacy/transforms/__tests__/remove-context-provider.test.ts new file mode 100644 index 0000000..b548563 --- /dev/null +++ b/codemods/legacy/transforms/__tests__/remove-context-provider.test.ts @@ -0,0 +1,51 @@ + +'use strict'; + +const tests = [ + 'with-provider', + 'with-provider-2', + 'no-provider', +]; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +describe('remove-context-provider', () => { + + tests.forEach(test => + defineTest( + __dirname, + 'remove-context-provider', + null, + `remove-context-provider/${ test }` + ) + ); + + + describe('typescript', () => { + + beforeEach(() => { + jest.mock('../remove-context-provider', () => { + return Object.assign( + jest.requireActual('../remove-context-provider'), + { + parser: 'tsx' + } + ); + }); + }); + + afterEach(() => { + jest.resetModules(); + }); + + tests.forEach(test => { + defineTest( + __dirname, + 'remove-context-provider', + null, + `remove-context-provider/typescript/${ test }` + ); + }); + + }); +}); diff --git a/codemods/legacy/transforms/__tests__/remove-forward-ref.test.ts b/codemods/legacy/transforms/__tests__/remove-forward-ref.test.ts new file mode 100644 index 0000000..000a7f6 --- /dev/null +++ b/codemods/legacy/transforms/__tests__/remove-forward-ref.test.ts @@ -0,0 +1,62 @@ + +'use strict'; + +const jsTests = [ + 'function-expression', + 'arrow-function-expression', + 'forward-ref-import', + 'forward-ref-import-2', + 'props-identifier', + 'props-object-pattern', + 'callee-is-member-expression' +]; + +const tsTests = [ + 'type-arguments', + 'type-arguments-custom-names', + 'type-arguments-type-literals', + 'props-type-literal' +]; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +describe('remove-forward-ref', () => { + + jsTests.forEach(test => + defineTest( + __dirname, + 'remove-forward-ref', + null, + `remove-forward-ref/${ test }` + ) + ); + + + describe('typescript', () => { + + beforeEach(() => { + jest.mock('../remove-forward-ref', () => { + return Object.assign( + jest.requireActual('../remove-forward-ref'), + { + parser: 'tsx' + } + ); + }); + }); + + afterEach(() => { + jest.resetModules(); + }); + + tsTests.forEach(test => { + defineTest( + __dirname, + 'remove-forward-ref', + null, + `remove-forward-ref/typescript/${ test }` + ); + }); + + }); +}); diff --git a/codemods/legacy/transforms/__tests__/rename-unsafe-lifecycles-test.js b/codemods/legacy/transforms/__tests__/rename-unsafe-lifecycles-test.js new file mode 100644 index 0000000..2c9f6c6 --- /dev/null +++ b/codemods/legacy/transforms/__tests__/rename-unsafe-lifecycles-test.js @@ -0,0 +1,74 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const tests = [ + 'arrow-functions', + 'create-react-class', + 'instance-methods', + 'manually-calling-lifecycles', + 'manually-invoked-mixin-methods', + 'one-lifecycle-calls-another', + 'standalone-function', + 'variable-within-class-method', +]; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +describe('rename-unsafe-lifecycles', () => { + describe('flow', () => { + beforeEach(() => { + jest.mock('../rename-unsafe-lifecycles', () => { + return Object.assign( + jest.requireActual('../rename-unsafe-lifecycles'), + { + parser: 'flow' + } + ); + }); + }); + + afterEach(() => { + jest.resetModules(); + }); + + tests.forEach(test => + defineTest( + __dirname, + 'rename-unsafe-lifecycles', + null, + `rename-unsafe-lifecycles/${test}` + ) + ); + }); + + describe('typescript', () => { + beforeEach(() => { + jest.mock('../rename-unsafe-lifecycles', () => { + return Object.assign( + jest.requireActual('../rename-unsafe-lifecycles'), + { + parser: 'tsx' + } + ); + }); + }); + + afterEach(() => { + jest.resetModules(); + }); + + defineTest( + __dirname, + 'rename-unsafe-lifecycles', + null, + 'rename-unsafe-lifecycles/typescript/class.tsx' + ); + }); +}); diff --git a/codemods/legacy/transforms/__tests__/sort-comp-test.js b/codemods/legacy/transforms/__tests__/sort-comp-test.js new file mode 100644 index 0000000..bdeb344 --- /dev/null +++ b/codemods/legacy/transforms/__tests__/sort-comp-test.js @@ -0,0 +1,15 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; +defineTest(__dirname, 'sort-comp'); +defineTest(__dirname, 'sort-comp', null, 'sort-comp2'); +defineTest(__dirname, 'sort-comp', null, 'sort-comp3'); +defineTest(__dirname, 'sort-comp', null, 'sort-comp-pure'); diff --git a/codemods/legacy/transforms/__tests__/update-react-imports-test.js b/codemods/legacy/transforms/__tests__/update-react-imports-test.js new file mode 100644 index 0000000..d9e8ecd --- /dev/null +++ b/codemods/legacy/transforms/__tests__/update-react-imports-test.js @@ -0,0 +1,94 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const flowOnlyTests = [ + 'flow-default-and-type-specifier-import-react-variable', + 'flow-default-and-type-specifier-import', + 'react-type-not-removed', + 'react-type-default-export', +]; + +const tsOnlyTests = [ + 'preserve-types-namespace', + 'preserve-types-default', +]; + +const tests = [ + 'default-and-multiple-specifiers-import-react-variable', + 'default-and-multiple-specifiers-import', + 'jsx-element', + 'jsx-fragment', + 'leading-comment', + 'react-basic-default-export-jsx-element-react-variable', + 'react-basic-default-export-jsx-element', + 'react-basic-default-export', + 'react-not-removed', + 'variable-already-used', + 'react-jsx-member-expression', + 'react-already-used-named-export', +]; + +const destructureNamedImportTests = [ + 'destructure-named-imports', + 'destructure-named-imports-variable-used', + 'destructure-named-imports-react-not-removed', +]; + +jest.mock('../update-react-imports', () => { + return Object.assign(jest.requireActual('../update-react-imports'), { + parser: 'flow', + }); +}); + +const defineTest = require('jscodeshift/dist/testUtils').defineTest; + +[...tests, ...flowOnlyTests].forEach((test) => { + defineTest( + __dirname, + 'update-react-imports', + null, + `update-react-imports/${test}` + ); +}); + +describe('typescript', () => { + beforeEach(() => { + jest.mock('../update-react-imports', () => { + return Object.assign( + jest.requireActual('../update-react-imports'), + { + parser: 'tsx' + } + ); + }); + }); + + afterEach(() => { + jest.resetModules(); + }); + + [...tests, ...tsOnlyTests].forEach((test) => { + defineTest( + __dirname, + 'update-react-imports', + null, + `update-react-imports/typescript/${test}.tsx` + ); + }); +}); + +destructureNamedImportTests.forEach((test) => { + defineTest( + __dirname, + 'update-react-imports', + {destructureNamespaceImports: true}, + `update-react-imports/${test}` + ); +}); diff --git a/codemods/legacy/transforms/class.js b/codemods/legacy/transforms/class.js new file mode 100644 index 0000000..ff7a212 --- /dev/null +++ b/codemods/legacy/transforms/class.js @@ -0,0 +1,1426 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const { basename, extname, dirname } = require('path'); + +module.exports = (file, api, options) => { + const j = api.jscodeshift; + + require('./utils/array-polyfills'); + const ReactUtils = require('./utils/ReactUtils')(j); + const doesNotUseArguments = require('./utils/doesNotUseArguments')(j); + + const printOptions = options.printOptions || { + quote: 'single', + trailingComma: true, + flowObjectCommas: true, + arrowParensAlways: true, + arrayBracketSpacing: false, + objectCurlySpacing: false + }; + + const root = j(file.source); + + // retain top comments + const { comments: topComments } = root.find(j.Program).get('body', 0).node; + + const AUTOBIND_IGNORE_KEYS = { + componentDidMount: true, + componentDidUpdate: true, + componentWillReceiveProps: true, + componentWillMount: true, + componentWillUpdate: true, + componentWillUnmount: true, + getChildContext: true, + getDefaultProps: true, + getInitialState: true, + render: true, + shouldComponentUpdate: true + }; + + const DEFAULT_PROPS_FIELD = 'getDefaultProps'; + const DEFAULT_PROPS_KEY = 'defaultProps'; + const GET_INITIAL_STATE_FIELD = 'getInitialState'; + + const DEPRECATED_APIS = [ + 'getDOMNode', + 'isMounted', + 'replaceProps', + 'replaceState', + 'setProps' + ]; + + const PURE_MIXIN_MODULE_NAME = + options['mixin-module-name'] || 'react-addons-pure-render-mixin'; + + const CREATE_CLASS_MODULE_NAME = + options['create-class-module-name'] || 'create-react-class'; + + const CREATE_CLASS_VARIABLE_NAME = + options['create-class-variable-name'] || 'createReactClass'; + + const STATIC_KEY = 'statics'; + + const STATIC_KEYS = { + childContextTypes: true, + contextTypes: true, + displayName: true, + propTypes: true + }; + + const MIXIN_KEY = 'mixins'; + + const NO_CONVERSION = options.conversion === false; + + const NO_DISPLAY_NAME = options['display-name'] === false; + + let shouldTransformFlow = false; + + if (options['flow']) { + const programBodyNode = root.find(j.Program).get('body', 0).node; + if (programBodyNode && programBodyNode.comments) { + programBodyNode.comments.forEach(node => { + if (node.value.indexOf('@flow') !== -1) { + shouldTransformFlow = true; + } + }); + } + } + + // --------------------------------------------------------------------------- + // Helpers + const createFindPropFn = prop => property => + property.key && + property.key.type === 'Identifier' && + property.key.name === prop; + + const filterDefaultPropsField = node => + createFindPropFn(DEFAULT_PROPS_FIELD)(node); + + const filterGetInitialStateField = node => + createFindPropFn(GET_INITIAL_STATE_FIELD)(node); + + const findGetInitialState = specPath => + specPath.properties.find(createFindPropFn(GET_INITIAL_STATE_FIELD)); + + const withComments = (to, from) => { + to.comments = from.comments; + return to; + }; + + const isPrimExpression = node => + node.type === 'Literal' || // NOTE this might change in babylon v6 + (node.type === 'Identifier' && node.name === 'undefined'); + + const isFunctionExpression = node => + node.key && + node.key.type === 'Identifier' && + node.value && + node.value.type === 'FunctionExpression'; + + const isPrimProperty = prop => + prop.key && + prop.key.type === 'Identifier' && + prop.value && + isPrimExpression(prop.value); + + const isPrimPropertyWithTypeAnnotation = prop => + prop.key && + prop.key.type === 'Identifier' && + prop.value && + prop.value.type === 'TypeCastExpression' && + isPrimExpression(prop.value.expression); + + const hasSingleReturnStatement = value => + (value.type === 'ArrowFunctionExpression' && + value.body && + value.body.type === 'ObjectExpression') || + ((value.type === 'FunctionExpression' || + value.type === 'ArrowFunctionExpression') && + value.body && + value.body.type === 'BlockStatement' && + value.body.body && + value.body.body.length === 1 && + value.body.body[0].type === 'ReturnStatement' && + value.body.body[0].argument); + + const isInitialStateLiftable = getInitialState => { + if (!getInitialState || !getInitialState.value) { + return true; + } + + return hasSingleReturnStatement(getInitialState.value); + }; + + // --------------------------------------------------------------------------- + // Checks if the module uses mixins or accesses deprecated APIs. + const checkDeprecatedAPICalls = classPath => + DEPRECATED_APIS.reduce( + (acc, name) => + acc + + j(classPath) + .find(j.Identifier, { name }) + .filter(path => { + // Do not consider history.replaceState() deprecated + let correctContext = true; + + if ( + name === 'replaceState' && + path.parentPath && + path.parentPath.value && + path.parentPath.value.object && + path.parentPath.value.object.name && + path.parentPath.value.object.name === 'history' + ) { + correctContext = false; + } + + return correctContext; + }) + .size(), + 0 + ) > 0; + + const hasNoCallsToDeprecatedAPIs = classPath => { + if (checkDeprecatedAPICalls(classPath)) { + console.warn( + file.path + + ': `' + + ReactUtils.directlyGetComponentName(classPath) + + '` ' + + 'was skipped because of deprecated API calls. Remove calls to ' + + DEPRECATED_APIS.join(', ') + + ' in your React component and re-run ' + + 'this script.' + ); + return false; + } + return true; + }; + + const hasNoRefsToAPIsThatWillBeRemoved = classPath => { + const hasInvalidCalls = + j(classPath) + .find(j.MemberExpression, { + object: { type: 'ThisExpression' }, + property: { name: DEFAULT_PROPS_FIELD } + }) + .size() > 0 || + j(classPath) + .find(j.MemberExpression, { + object: { type: 'ThisExpression' }, + property: { name: GET_INITIAL_STATE_FIELD } + }) + .size() > 0; + + if (hasInvalidCalls) { + console.warn( + file.path + + ': `' + + ReactUtils.directlyGetComponentName(classPath) + + '` ' + + 'was skipped because of API calls that will be removed. Remove calls to `' + + DEFAULT_PROPS_FIELD + + '` and/or `' + + GET_INITIAL_STATE_FIELD + + '` in your React component and re-run this script.' + ); + return false; + } + return true; + }; + + const isGetInitialStateConstructorSafe = getInitialState => { + if (!getInitialState) { + return true; + } + + const collection = j(getInitialState); + let result = true; + + const propsVarDeclarationCount = collection + .find(j.VariableDeclarator, { + id: { name: 'props' } + }) + .size(); + + const contextVarDeclarationCount = collection + .find(j.VariableDeclarator, { + id: { name: 'context' } + }) + .size(); + + if ( + propsVarDeclarationCount && + propsVarDeclarationCount !== + collection + .find(j.VariableDeclarator, { + id: { name: 'props' }, + init: { + type: 'MemberExpression', + object: { type: 'ThisExpression' }, + property: { name: 'props' } + } + }) + .size() + ) { + result = false; + } + + if ( + contextVarDeclarationCount && + contextVarDeclarationCount !== + collection + .find(j.VariableDeclarator, { + id: { name: 'context' }, + init: { + type: 'MemberExpression', + object: { type: 'ThisExpression' }, + property: { name: 'context' } + } + }) + .size() + ) { + result = false; + } + + return result; + }; + + const isInitialStateConvertible = classPath => { + const specPath = ReactUtils.directlyGetCreateClassSpec(classPath); + if (!specPath) { + return false; + } + const result = isGetInitialStateConstructorSafe( + findGetInitialState(specPath) + ); + if (!result) { + console.warn( + file.path + + ': `' + + ReactUtils.directlyGetComponentName(classPath) + + '` ' + + 'was skipped because of potential shadowing issues were found in ' + + 'the React component. Rename variable declarations of `props` and/or `context` ' + + 'in your `getInitialState` and re-run this script.' + ); + } + return result; + }; + + const canConvertToClass = classPath => { + const specPath = ReactUtils.directlyGetCreateClassSpec(classPath); + if (!specPath) { + return false; + } + const invalidProperties = specPath.properties.filter( + prop => + !prop.key.name || + (!STATIC_KEYS.hasOwnProperty(prop.key.name) && + STATIC_KEY != prop.key.name && + !filterDefaultPropsField(prop) && + !filterGetInitialStateField(prop) && + !isFunctionExpression(prop) && + !isPrimProperty(prop) && + !isPrimPropertyWithTypeAnnotation(prop) && + MIXIN_KEY != prop.key.name) + ); + + if (invalidProperties.length) { + const invalidText = invalidProperties + .map(prop => (prop.key.name ? prop.key.name : prop.key)) + .join(', '); + console.warn( + file.path + + ': `' + + ReactUtils.directlyGetComponentName(classPath) + + '` ' + + 'was skipped because of invalid field(s) `' + + invalidText + + '` on ' + + 'the React component. Remove any right-hand-side expressions that ' + + 'are not simple, like: `componentWillUpdate: createWillUpdate()` or ' + + '`render: foo ? renderA : renderB`.' + ); + } + return !invalidProperties.length; + }; + + const areMixinsConvertible = (mixinIdentifierNames, classPath) => { + if ( + ReactUtils.directlyHasMixinsField(classPath) && + !ReactUtils.directlyHasSpecificMixins(classPath, mixinIdentifierNames) + ) { + return false; + } + return true; + }; + + // --------------------------------------------------------------------------- + // Collectors + const pickReturnValueOrCreateIIFE = value => { + if (hasSingleReturnStatement(value)) { + if (value.body.type === 'ObjectExpression') { + return value.body; + } else { + return value.body.body[0].argument; + } + } else { + return j.callExpression(value, []); + } + }; + + const createDefaultProps = prop => + withComments( + j.property( + 'init', + j.identifier(DEFAULT_PROPS_KEY), + pickReturnValueOrCreateIIFE(prop.value) + ), + prop + ); + + // Collects `childContextTypes`, `contextTypes`, `displayName`, and `propTypes`; + // simplifies `getDefaultProps` or converts it to an IIFE; + // and collects everything else in the `statics` property object. + const collectStatics = specPath => { + const result = []; + + for (let i = 0; i < specPath.properties.length; i++) { + const property = specPath.properties[i]; + if ( + createFindPropFn('statics')(property) && + property.value && + property.value.properties + ) { + result.push(...property.value.properties); + } else if (createFindPropFn(DEFAULT_PROPS_FIELD)(property)) { + result.push(createDefaultProps(property)); + } else if ( + property.key && + STATIC_KEYS.hasOwnProperty(property.key.name) + ) { + result.push(property); + } + } + + return result; + }; + + const collectNonStaticProperties = specPath => + specPath.properties + .filter( + prop => + !(filterDefaultPropsField(prop) || filterGetInitialStateField(prop)) + ) + .filter( + prop => + !STATIC_KEYS.hasOwnProperty(prop.key.name) && + prop.key.name !== STATIC_KEY + ) + .filter( + prop => + isFunctionExpression(prop) || + isPrimPropertyWithTypeAnnotation(prop) || + isPrimProperty(prop) + ); + + const findRequirePathAndBinding = moduleName => { + let result = null; + const requireCall = root.find(j.VariableDeclarator, { + id: { type: 'Identifier' }, + init: { + callee: { name: 'require' }, + arguments: [{ value: moduleName }] + } + }); + + const importStatement = root.find(j.ImportDeclaration, { + source: { + value: moduleName + } + }); + + if (importStatement.size()) { + importStatement.forEach(path => { + result = { + path, + binding: path.value.specifiers[0].local.name, + type: 'import' + }; + }); + } else if (requireCall.size()) { + requireCall.forEach(path => { + result = { + path, + binding: path.value.id.name, + type: 'require' + }; + }); + } + + return result; + }; + + const pureRenderMixinPathAndBinding = findRequirePathAndBinding( + PURE_MIXIN_MODULE_NAME + ); + + // --------------------------------------------------------------------------- + // Boom! + const createMethodDefinition = fn => + withComments(j.methodDefinition('method', fn.key, fn.value), fn); + + const updatePropsAndContextAccess = getInitialState => { + const collection = j(getInitialState); + + collection + .find(j.MemberExpression, { + object: { + type: 'ThisExpression' + }, + property: { + type: 'Identifier', + name: 'props' + } + }) + .forEach(path => j(path).replaceWith(j.identifier('props'))); + + collection + .find(j.MemberExpression, { + object: { + type: 'ThisExpression' + }, + property: { + type: 'Identifier', + name: 'context' + } + }) + .forEach(path => j(path).replaceWith(j.identifier('context'))); + }; + + const inlineGetInitialState = getInitialState => { + const functionExpressionCollection = j(getInitialState.value); + + // at this point if there exists bindings like `const props = ...`, we + // already know the RHS must be `this.props` (see `isGetInitialStateConstructorSafe`) + // so it's safe to just remove them + functionExpressionCollection + .find(j.VariableDeclarator, { id: { name: 'props' } }) + .forEach(path => j(path).remove()); + + functionExpressionCollection + .find(j.VariableDeclarator, { id: { name: 'context' } }) + .forEach(path => j(path).remove()); + + return functionExpressionCollection + .find(j.ReturnStatement) + .filter(path => { + // filter out inner function declarations here (helper functions, promises, etc.). + const mainBodyCollection = j(getInitialState.value.body); + return ( + mainBodyCollection + .find(j.ArrowFunctionExpression) + .find(j.ReturnStatement, path.value) + .size() === 0 && + mainBodyCollection + .find(j.FunctionDeclaration) + .find(j.ReturnStatement, path.value) + .size() === 0 && + mainBodyCollection + .find(j.FunctionExpression) + .find(j.ReturnStatement, path.value) + .size() === 0 + ); + }) + .forEach(path => { + let shouldInsertReturnAfterAssignment = false; + + // if the return statement is not a direct child of getInitialState's body + if (getInitialState.value.body.body.indexOf(path.value) === -1) { + shouldInsertReturnAfterAssignment = true; + } + + j(path).replaceWith( + j.expressionStatement( + j.assignmentExpression( + '=', + j.memberExpression( + j.thisExpression(), + j.identifier('state'), + false + ), + path.value.argument + ) + ) + ); + + if (shouldInsertReturnAfterAssignment) { + j(path).insertAfter(j.returnStatement(null)); + } + }) + .getAST()[0].value.body.body; + }; + + const convertInitialStateToClassProperty = getInitialState => + withComments( + j.classProperty( + j.identifier('state'), + pickReturnValueOrCreateIIFE(getInitialState.value), + getInitialState.value.returnType, + false + ), + getInitialState + ); + + const createConstructorArgs = hasContextAccess => { + if (hasContextAccess) { + return [j.identifier('props'), j.identifier('context')]; + } + + return [j.identifier('props')]; + }; + + const createConstructor = getInitialState => { + const initialStateAST = j(getInitialState); + let hasContextAccess = false; + + if ( + initialStateAST + .find(j.MemberExpression, { + // has `this.context` access + object: { type: 'ThisExpression' }, + property: { type: 'Identifier', name: 'context' } + }) + .size() || + initialStateAST + .find(j.CallExpression, { + // a direct method call `this.x()` + callee: { + type: 'MemberExpression', + object: { type: 'ThisExpression' } + } + }) + .size() || + initialStateAST + .find(j.MemberExpression, { + // `this` is referenced alone + object: { type: 'ThisExpression' } + }) + .size() !== initialStateAST.find(j.ThisExpression).size() + ) { + hasContextAccess = true; + } + + updatePropsAndContextAccess(getInitialState); + const constructorArgs = createConstructorArgs(hasContextAccess); + + return [ + createMethodDefinition({ + key: j.identifier('constructor'), + value: j.functionExpression( + null, + constructorArgs, + j.blockStatement( + [].concat( + [ + j.expressionStatement( + j.callExpression(j.identifier('super'), constructorArgs) + ) + ], + inlineGetInitialState(getInitialState) + ) + ) + ) + }) + ]; + }; + + const createArrowFunctionExpression = fn => { + const arrowFunc = j.arrowFunctionExpression(fn.params, fn.body, false); + + arrowFunc.returnType = fn.returnType; + arrowFunc.defaults = fn.defaults; + arrowFunc.rest = fn.rest; + arrowFunc.async = fn.async; + arrowFunc.generator = fn.generator; + + return arrowFunc; + }; + + const createArrowProperty = prop => + withComments( + j.classProperty( + j.identifier(prop.key.name), + createArrowFunctionExpression(prop.value), + null, + false + ), + prop + ); + + const createClassProperty = prop => + withComments( + j.classProperty(j.identifier(prop.key.name), prop.value, null, false), + prop + ); + + const createClassPropertyWithType = prop => + withComments( + j.classProperty( + j.identifier(prop.key.name), + prop.value.expression, + prop.value.typeAnnotation, + false + ), + prop + ); + + // --------------------------------------------------------------------------- + // Flow! + + const flowAnyType = j.anyTypeAnnotation(); + const flowFixMeType = j.genericTypeAnnotation( + j.identifier('$FlowFixMe'), + null + ); + + const literalToFlowType = node => { + if (node.type === 'Identifier' && node.name === 'undefined') { + return j.voidTypeAnnotation(); + } + + switch (typeof node.value) { + case 'string': + return j.stringLiteralTypeAnnotation(node.value, node.raw); + case 'number': + return j.numberLiteralTypeAnnotation(node.value, node.raw); + case 'boolean': + return j.booleanLiteralTypeAnnotation(node.value, node.raw); + case 'object': // we already know it's a NullLiteral here + return j.nullLiteralTypeAnnotation(); + default: + // this should never happen + return flowFixMeType; + } + }; + + const propTypeToFlowMapping = { + // prim types + any: flowAnyType, + array: j.genericTypeAnnotation( + j.identifier('Array'), + j.typeParameterInstantiation([flowFixMeType]) + ), + bool: j.booleanTypeAnnotation(), + element: flowFixMeType, // flow does the same for `element` type in `propTypes` + func: j.genericTypeAnnotation(j.identifier('Function'), null), + node: flowFixMeType, // flow does the same for `node` type in `propTypes` + number: j.numberTypeAnnotation(), + object: j.genericTypeAnnotation(j.identifier('Object'), null), + string: j.stringTypeAnnotation(), + + // type classes + arrayOf: type => + j.genericTypeAnnotation( + j.identifier('Array'), + j.typeParameterInstantiation([type]) + ), + instanceOf: type => j.genericTypeAnnotation(type, null), + objectOf: type => + j.objectTypeAnnotation( + [], + [ + j.objectTypeIndexer( + j.identifier('key'), + j.stringTypeAnnotation(), + type + ) + ] + ), + oneOf: typeList => j.unionTypeAnnotation(typeList), + oneOfType: typeList => j.unionTypeAnnotation(typeList), + shape: propList => j.objectTypeAnnotation(propList) + }; + + const propTypeToFlowAnnotation = val => { + let cursor = val; + let isOptional = true; + let typeResult = flowFixMeType; + + if ( + // check `.isRequired` first + cursor.type === 'MemberExpression' && + cursor.property.type === 'Identifier' && + cursor.property.name === 'isRequired' + ) { + isOptional = false; + cursor = cursor.object; + } + + switch (cursor.type) { + case 'CallExpression': { + // type class + const calleeName = + cursor.callee.type === 'MemberExpression' + ? cursor.callee.property.name + : cursor.callee.name; + + const constructor = propTypeToFlowMapping[calleeName]; + if (!constructor) { + // unknown type class + // it's not necessary since `typeResult` defaults to `flowFixMeType`, + // but it's more explicit this way + typeResult = flowFixMeType; + break; + } + + switch (cursor.callee.property.name) { + case 'arrayOf': { + const arg = cursor.arguments[0]; + typeResult = constructor(propTypeToFlowAnnotation(arg)[0]); + break; + } + case 'instanceOf': { + const arg = cursor.arguments[0]; + if (arg.type !== 'Identifier') { + typeResult = flowFixMeType; + break; + } + + typeResult = constructor(arg); + break; + } + case 'objectOf': { + const arg = cursor.arguments[0]; + typeResult = constructor(propTypeToFlowAnnotation(arg)[0]); + break; + } + case 'oneOf': { + const argList = cursor.arguments[0].elements; + if ( + !argList || + !argList.every( + node => + node.type === 'Literal' || + (node.type === 'Identifier' && node.name === 'undefined') + ) + ) { + typeResult = flowFixMeType; + } else { + typeResult = constructor(argList.map(literalToFlowType)); + } + break; + } + case 'oneOfType': { + const argList = cursor.arguments[0].elements; + if (!argList) { + typeResult = flowFixMeType; + } else { + typeResult = constructor( + argList.map(arg => propTypeToFlowAnnotation(arg)[0]) + ); + } + break; + } + case 'shape': { + const rawPropList = cursor.arguments[0].properties; + if (!rawPropList) { + typeResult = flowFixMeType; + break; + } + const flowPropList = []; + rawPropList.forEach(typeProp => { + const keyIsLiteral = typeProp.key.type === 'Literal'; + const name = keyIsLiteral + ? typeProp.key.value + : typeProp.key.name; + + const [valueType, isOptional] = propTypeToFlowAnnotation( + typeProp.value + ); + flowPropList.push( + j.objectTypeProperty( + keyIsLiteral ? j.literal(name) : j.identifier(name), + valueType, + isOptional + ) + ); + }); + + typeResult = constructor(flowPropList); + break; + } + default: { + break; + } + } + break; + } + case 'MemberExpression': { + // prim type + if (cursor.property.type !== 'Identifier') { + // unrecognizable + typeResult = flowFixMeType; + break; + } + + const maybeType = propTypeToFlowMapping[cursor.property.name]; + if (maybeType) { + typeResult = propTypeToFlowMapping[cursor.property.name]; + } else { + // type not found + typeResult = flowFixMeType; + } + + break; + } + default: { + // unrecognizable + break; + } + } + + return [typeResult, isOptional]; + }; + + const createFlowAnnotationsFromPropTypesProperties = prop => { + const typePropertyList = []; + + if (!prop || prop.value.type !== 'ObjectExpression') { + return typePropertyList; + } + + prop.value.properties.forEach(typeProp => { + if (!typeProp.key) { + // stuff like SpreadProperty + return; + } + + const keyIsLiteral = typeProp.key.type === 'Literal'; + const name = keyIsLiteral ? typeProp.key.value : typeProp.key.name; + + const [valueType, isOptional] = propTypeToFlowAnnotation(typeProp.value); + typePropertyList.push( + j.objectTypeProperty( + keyIsLiteral ? j.literal(name) : j.identifier(name), + valueType, + isOptional + ) + ); + }); + + return j.classProperty( + j.identifier('props'), + null, + j.typeAnnotation(j.objectTypeAnnotation(typePropertyList)), + false + ); + }; + + // to ensure that our property initializers' evaluation order is safe + const repositionStateProperty = ( + initialStateProperty, + propertiesAndMethods + ) => { + const initialStateCollection = j(initialStateProperty); + const thisCount = initialStateCollection.find(j.ThisExpression).size(); + const safeThisMemberCount = + initialStateCollection + .find(j.MemberExpression, { + object: { + type: 'ThisExpression' + }, + property: { + type: 'Identifier', + name: 'props' + } + }) + .size() + + initialStateCollection + .find(j.MemberExpression, { + object: { + type: 'ThisExpression' + }, + property: { + type: 'Identifier', + name: 'context' + } + }) + .size(); + + if (thisCount === safeThisMemberCount) { + return initialStateProperty.concat(propertiesAndMethods); + } + + const result = [].concat(propertiesAndMethods); + let lastPropPosition = result.length - 1; + + while ( + lastPropPosition >= 0 && + result[lastPropPosition].kind === 'method' + ) { + lastPropPosition--; + } + + result.splice(lastPropPosition + 1, 0, initialStateProperty[0]); + return result; + }; + + // if there's no `getInitialState` or the `getInitialState` function is simple + // (i.e., it's just a return statement) then we don't need a constructor. + // we can simply lift `state = {...}` as a property initializer. + // otherwise, create a constructor and inline `this.state = ...`. + // + // when we need to create a constructor, we only put `context` as the + // second parameter when the following things happen in `getInitialState()`: + // 1. there's a `this.context` access, or + // 2. there's a direct method call `this.x()`, or + // 3. `this` is referenced alone + const createESClass = ( + name, + baseClassName, + staticProperties, + getInitialState, + rawProperties, + comments + ) => { + const initialStateProperty = []; + let maybeConstructor = []; + let maybeFlowStateAnnotation = []; // we only need this when we do `this.state = ...` + + if (isInitialStateLiftable(getInitialState)) { + if (getInitialState) { + initialStateProperty.push( + convertInitialStateToClassProperty(getInitialState) + ); + } + } else { + maybeConstructor = createConstructor(getInitialState); + if (shouldTransformFlow) { + let stateType = j.typeAnnotation(j.existsTypeAnnotation()); + + if (getInitialState.value.returnType) { + stateType = getInitialState.value.returnType; + } + + maybeFlowStateAnnotation.push( + j.classProperty(j.identifier('state'), null, stateType, false) + ); + } + } + + const propertiesAndMethods = rawProperties.map(prop => { + if (isPrimPropertyWithTypeAnnotation(prop)) { + return createClassPropertyWithType(prop); + } else if (isPrimProperty(prop)) { + return createClassProperty(prop); + } else if (AUTOBIND_IGNORE_KEYS.hasOwnProperty(prop.key.name)) { + return createMethodDefinition(prop); + } + + return createArrowProperty(prop); + }); + + const flowPropsAnnotation = shouldTransformFlow + ? createFlowAnnotationsFromPropTypesProperties( + staticProperties.find(path => path.key.name === 'propTypes') + ) + : []; + + let finalStaticProperties = staticProperties; + + if (shouldTransformFlow && options['remove-runtime-proptypes']) { + finalStaticProperties = staticProperties.filter( + prop => prop.key.name !== 'propTypes' + ); + } + + return withComments( + j.classDeclaration( + name ? j.identifier(name) : null, + j.classBody( + [].concat( + flowPropsAnnotation, + maybeFlowStateAnnotation, + finalStaticProperties, + maybeConstructor, + repositionStateProperty(initialStateProperty, propertiesAndMethods) + ) + ), + j.memberExpression( + j.identifier('React'), + j.identifier(baseClassName), + false + ) + ), + { comments } + ); + }; + + const createStaticClassProperty = staticProperty => { + if (staticProperty.value.type === 'FunctionExpression') { + return withComments( + j.methodDefinition( + 'method', + j.identifier(staticProperty.key.name), + staticProperty.value, + true + ), + staticProperty + ); + } + + if (staticProperty.value.type === 'TypeCastExpression') { + return withComments( + j.classProperty( + j.identifier(staticProperty.key.name), + staticProperty.value.expression, + staticProperty.value.typeAnnotation, + true + ), + staticProperty + ); + } + + return withComments( + j.classProperty( + j.identifier(staticProperty.key.name), + staticProperty.value, + null, + true + ), + staticProperty + ); + }; + + const createStaticClassProperties = statics => + statics.map(createStaticClassProperty); + + const getComments = classPath => { + if (classPath.value.comments) { + return classPath.value.comments; + } + const declaration = j(classPath).closest(j.VariableDeclaration); + if (declaration.size()) { + return declaration.get().value.comments; + } + return null; + }; + + const findUnusedVariables = (path, varName) => + j(path) + .closestScope() + .find(j.Identifier, { name: varName }) + // Ignore require vars + .filter(identifierPath => identifierPath.value !== path.value.id) + // Ignore import bindings + .filter( + identifierPath => + !( + path.value.type === 'ImportDeclaration' && + path.value.specifiers.some( + specifier => specifier.local === identifierPath.value + ) + ) + ) + // Ignore properties in MemberExpressions + .filter(identifierPath => { + const parent = identifierPath.parent.value; + return !( + j.MemberExpression.check(parent) && + parent.property === identifierPath.value + ); + }); + + const updateToClass = classPath => { + const specPath = ReactUtils.directlyGetCreateClassSpec(classPath); + const name = ReactUtils.directlyGetComponentName(classPath); + const statics = collectStatics(specPath); + const properties = collectNonStaticProperties(specPath); + const comments = getComments(classPath); + + const getInitialState = findGetInitialState(specPath); + + var path = classPath; + + if ( + classPath.parentPath && + classPath.parentPath.value && + classPath.parentPath.value.type === 'VariableDeclarator' + ) { + // the reason that we need to do this awkward dance here is that + // for things like `var Foo = React.createClass({...})`, we need to + // replace the _entire_ VariableDeclaration with + // `class Foo extends React.Component {...}`. + // it looks scary but since we already know it's a VariableDeclarator + // it's actually safe. + // (VariableDeclaration > declarations > VariableDeclarator > CallExpression) + path = classPath.parentPath.parentPath.parentPath; + } + + const staticProperties = createStaticClassProperties(statics); + const baseClassName = + pureRenderMixinPathAndBinding && + ReactUtils.directlyHasSpecificMixins(classPath, [ + pureRenderMixinPathAndBinding.binding + ]) + ? 'PureComponent' + : 'Component'; + + j(path).replaceWith( + createESClass( + name, + baseClassName, + staticProperties, + getInitialState, + properties, + comments + ) + ); + }; + + const addDisplayName = (displayName, specPath) => { + const props = specPath.properties; + let safe = true; + + for (let i = 0; i < props.length; i++) { + const prop = props[i]; + if (prop.key.name === 'displayName') { + safe = false; + break; + } + } + + if (safe) { + props.unshift( + j.objectProperty( + j.identifier('displayName'), + j.stringLiteral(displayName) + ) + ); + } + }; + + const fallbackToCreateClassModule = classPath => { + const comments = getComments(classPath); + const specPath = ReactUtils.directlyGetCreateClassSpec(classPath); + + if (!NO_DISPLAY_NAME) { + if (specPath) { + // Add a displayName property to the spec object + let path = classPath; + let displayName; + while (path && displayName === undefined) { + switch (path.node.type) { + case 'ExportDefaultDeclaration': + displayName = basename(file.path, extname(file.path)); + if (displayName === 'index') { + // ./{module name}/index.js + displayName = basename(dirname(file.path)); + } + break; + case 'VariableDeclarator': + displayName = path.node.id.name; + break; + case 'AssignmentExpression': + displayName = path.node.left.name; + break; + case 'Property': + displayName = path.node.key.name; + break; + case 'Statement': + displayName = null; + break; + } + path = path.parent; + } + if (displayName) { + addDisplayName(displayName, specPath); + } + } + } + + withComments( + j(classPath).replaceWith( + specPath + ? j.callExpression(j.identifier(CREATE_CLASS_VARIABLE_NAME), [ + specPath + ]) + : j.callExpression( + j.identifier(CREATE_CLASS_VARIABLE_NAME), + classPath.value.arguments + ) + ), + { comments } + ); + }; + + if (options['explicit-require'] === false || ReactUtils.hasReact(root)) { + // no mixins found on the classPath -> true + // pure mixin identifier not found -> (has mixins) -> false + // found pure mixin identifier -> + // class mixins is an array and only contains the identifier -> true + // otherwise -> false + const mixinsFilter = classPath => { + if (!ReactUtils.directlyHasMixinsField(classPath)) { + return true; + } else if (options['pure-component'] && pureRenderMixinPathAndBinding) { + const { binding } = pureRenderMixinPathAndBinding; + if (areMixinsConvertible([binding], classPath)) { + return true; + } + } + console.warn( + file.path + + ': `' + + ReactUtils.directlyGetComponentName(classPath) + + '` ' + + 'was skipped because of inconvertible mixins.' + ); + + return false; + }; + + const reinsertTopComments = () => { + root.get().node.comments = topComments; + }; + + let didTransform = false; + let didFallback = false; + + const path = ReactUtils.findAllReactCreateClassCalls(root); + if (NO_CONVERSION) { + path.forEach(childPath => { + fallbackToCreateClassModule(childPath); + }); + didFallback = true; + } else { + // the only time that we can't simply replace the createClass call path + // with a new class is when the parent of that is a variable declaration. + // let's delay it and figure it out later (by looking at `path.parentPath`) + // in `updateToClass`. + path.forEach(childPath => { + if ( + mixinsFilter(childPath) && + hasNoCallsToDeprecatedAPIs(childPath) && + hasNoRefsToAPIsThatWillBeRemoved(childPath) && + doesNotUseArguments(childPath) && + isInitialStateConvertible(childPath) && + canConvertToClass(childPath) + ) { + didTransform = true; + updateToClass(childPath); + } else { + didFallback = true; + fallbackToCreateClassModule(childPath); + } + }); + } + + if (didFallback) { + const reactPathAndBinding = + findRequirePathAndBinding('react') || + findRequirePathAndBinding('React'); + + if (reactPathAndBinding) { + const { path, type } = reactPathAndBinding; + let removePath = null; + let shouldReinsertComment = false; + if (type === 'require') { + const kind = path.parent.value.kind; + j(path.parent).insertAfter( + j.template.statement([ + `${kind} ${CREATE_CLASS_VARIABLE_NAME} = require('${CREATE_CLASS_MODULE_NAME}');` + ]) + ); + const bodyNode = path.parentPath.parentPath.parentPath.value; + const variableDeclarationNode = path.parentPath.parentPath.value; + shouldReinsertComment = + bodyNode.indexOf(variableDeclarationNode) === 0; + removePath = path.parent; + } else { + j(path).insertAfter( + j.template.statement([ + `import ${CREATE_CLASS_VARIABLE_NAME} from '${CREATE_CLASS_MODULE_NAME}';` + ]) + ); + const importDeclarationNode = path.value; + const bodyNode = path.parentPath.value; + removePath = path; + const specifiers = path.value.specifiers; + if (specifiers.length === 1) { + shouldReinsertComment = + bodyNode.indexOf(importDeclarationNode) === 0; + removePath = path; + } else { + const paths = j(path).find(j.ImportDefaultSpecifier); + if (paths.length) { + removePath = j(path) + .find(j.ImportDefaultSpecifier) + .paths()[0]; + } + } + } + + const shouldRemoveReactImport = + removePath && + root.find(j.Identifier).filter(path => path.value.name === 'React') + .length === 1 && + root.find(j.JSXElement).length === 0; + if (shouldRemoveReactImport && removePath) { + j(removePath).remove(); + if (shouldReinsertComment) { + reinsertTopComments(); + } + } + } + } + + if (didTransform) { + // prune removed requires + if (pureRenderMixinPathAndBinding) { + const { binding, path, type } = pureRenderMixinPathAndBinding; + let shouldReinsertComment = false; + if (findUnusedVariables(path, binding).size() === 0) { + var removePath = null; + if (type === 'require') { + const bodyNode = path.parentPath.parentPath.parentPath.value; + const variableDeclarationNode = path.parentPath.parentPath.value; + + if (variableDeclarationNode.declarations.length === 1) { + removePath = path.parentPath.parentPath; + shouldReinsertComment = + bodyNode.indexOf(variableDeclarationNode) === 0; + } else { + removePath = path; + } + } else { + const importDeclarationNode = path.value; + const bodyNode = path.parentPath.value; + + removePath = path; + shouldReinsertComment = + bodyNode.indexOf(importDeclarationNode) === 0; + } + + j(removePath).remove(); + if (shouldReinsertComment) { + reinsertTopComments(); + } + } + } + } + } + return root.toSource(printOptions); +}; + +module.exports.parser = 'flow'; diff --git a/codemods/legacy/transforms/create-element-to-jsx.js b/codemods/legacy/transforms/create-element-to-jsx.js new file mode 100644 index 0000000..66dbb88 --- /dev/null +++ b/codemods/legacy/transforms/create-element-to-jsx.js @@ -0,0 +1,269 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +module.exports = function(file, api, options) { + const j = api.jscodeshift; + const printOptions = options.printOptions || {}; + const root = j(file.source); + const ReactUtils = require('./utils/ReactUtils')(j); + const encodeJSXTextValue = value => + value.replace(//g, '>'); + + const canLiteralBePropString = node => + node.raw.indexOf('\\') === -1 && node.value.indexOf('"') === -1; + + const convertExpressionToJSXAttributes = expression => { + if (!expression) { + return { + attributes: [], + extraComments: [] + }; + } + + const isReactSpread = + expression.type === 'CallExpression' && + expression.callee.type === 'MemberExpression' && + expression.callee.object.name === 'React' && + expression.callee.property.name === '__spread'; + + const isObjectAssign = + expression.type === 'CallExpression' && + expression.callee.type === 'MemberExpression' && + expression.callee.object.name === 'Object' && + expression.callee.property.name === 'assign'; + + const validSpreadTypes = [ + 'Identifier', + 'MemberExpression', + 'CallExpression' + ]; + + if (isReactSpread || isObjectAssign) { + const resultAttributes = []; + const resultExtraComments = expression.comments || []; + const { callee } = expression; + for (const node of [callee, callee.object, callee.property]) { + resultExtraComments.push(...(node.comments || [])); + } + expression.arguments.forEach(expression => { + const { attributes, extraComments } = convertExpressionToJSXAttributes( + expression + ); + resultAttributes.push(...attributes); + resultExtraComments.push(...extraComments); + }); + + return { + attributes: resultAttributes, + extraComments: resultExtraComments + }; + } else if (validSpreadTypes.indexOf(expression.type) != -1) { + return { + attributes: [j.jsxSpreadAttribute(expression)], + extraComments: [] + }; + } else if (expression.type === 'ObjectExpression') { + const attributes = expression.properties.map(property => { + if (property.type === 'SpreadProperty') { + const spreadAttribute = j.jsxSpreadAttribute(property.argument); + spreadAttribute.comments = property.comments; + return spreadAttribute; + } else if (property.type === 'Property') { + const propertyValueType = property.value.type; + + let value; + if ( + propertyValueType === 'Literal' && + typeof property.value.value === 'string' && + canLiteralBePropString(property.value) + ) { + value = j.literal(property.value.value); + value.comments = property.value.comments; + } else { + value = j.jsxExpressionContainer(property.value); + } + + let jsxIdentifier; + if (property.key.type === 'Literal') { + jsxIdentifier = j.jsxIdentifier(property.key.value); + } else { + jsxIdentifier = j.jsxIdentifier(property.key.name); + } + jsxIdentifier.comments = property.key.comments; + + const jsxAttribute = j.jsxAttribute(jsxIdentifier, value); + jsxAttribute.comments = property.comments; + return jsxAttribute; + } + return null; + }); + + return { + attributes, + extraComments: expression.comments || [] + }; + } else if (expression.type === 'Literal' && expression.value === null) { + return { + attributes: [], + extraComments: expression.comments || [] + }; + } else { + throw new Error(`Unexpected attribute of type "${expression.type}"`); + } + }; + + const canConvertToJSXIdentifier = node => + (node.type === 'Literal' && typeof node.value === 'string') || + node.type === 'Identifier' || + (node.type === 'MemberExpression' && + !node.computed && + canConvertToJSXIdentifier(node.object) && + canConvertToJSXIdentifier(node.property)); + + const jsxIdentifierFor = node => { + let identifier; + let comments = node.comments || []; + if (node.type === 'Literal') { + identifier = j.jsxIdentifier(node.value); + } else if (node.type === 'MemberExpression') { + let { + identifier: objectIdentifier, + comments: objectComments + } = jsxIdentifierFor(node.object); + let { + identifier: propertyIdentifier, + comments: propertyComments + } = jsxIdentifierFor(node.property); + identifier = j.jsxMemberExpression(objectIdentifier, propertyIdentifier); + comments.push(...objectComments, ...propertyComments); + } else { + identifier = j.jsxIdentifier(node.name); + } + return { identifier, comments }; + }; + + const isCapitalizationInvalid = node => + (node.type === 'Literal' && !/^[a-z]/.test(node.value)) || + (node.type === 'Identifier' && /^[a-z]/.test(node.name)); + + const convertNodeToJSX = node => { + const comments = node.value.comments || []; + const { callee } = node.value; + for (const calleeNode of [callee, callee.object, callee.property]) { + for (const comment of calleeNode.comments || []) { + comment.leading = true; + comment.trailing = false; + comments.push(comment); + } + } + + const args = node.value.arguments; + + if ( + isCapitalizationInvalid(args[0]) || + !canConvertToJSXIdentifier(args[0]) + ) { + return node.value; + } + + const { + identifier: jsxIdentifier, + comments: identifierComments + } = jsxIdentifierFor(args[0]); + const props = args[1]; + + const { attributes, extraComments } = convertExpressionToJSXAttributes( + props + ); + + for (const comment of [...identifierComments, ...extraComments]) { + comment.leading = false; + comment.trailing = true; + comments.push(comment); + } + + const children = args.slice(2).map((child, index) => { + if ( + child.type === 'Literal' && + typeof child.value === 'string' && + !child.comments && + child.value !== '' && + child.value.trim() === child.value + ) { + return j.jsxText(encodeJSXTextValue(child.value)); + } else if ( + child.type === 'CallExpression' && + child.callee.object && + child.callee.object.name === 'React' && + child.callee.property.name === 'createElement' + ) { + const jsxChild = convertNodeToJSX(node.get('arguments', index + 2)); + if ( + jsxChild.type !== 'JSXElement' || + (jsxChild.comments || []).length > 0 + ) { + return j.jsxExpressionContainer(jsxChild); + } else { + return jsxChild; + } + } else if (child.type === 'SpreadElement') { + return j.jsxExpressionContainer(child.argument); + } else { + return j.jsxExpressionContainer(child); + } + }); + + const openingElement = j.jsxOpeningElement(jsxIdentifier, attributes); + + if (children.length) { + const endIdentifier = Object.assign({}, jsxIdentifier, { comments: [] }); + // Add text newline nodes between elements so recast formats one child per + // line instead of all children on one line. + const paddedChildren = [j.jsxText('\n')]; + for (const child of children) { + paddedChildren.push(child, j.jsxText('\n')); + } + const element = j.jsxElement( + openingElement, + j.jsxClosingElement(endIdentifier), + paddedChildren + ); + element.comments = comments; + return element; + } else { + openingElement.selfClosing = true; + const element = j.jsxElement(openingElement); + element.comments = comments; + return element; + } + }; + + if (options['explicit-require'] === false || ReactUtils.hasReact(root)) { + const mutations = root + .find(j.CallExpression, { + callee: { + object: { + name: 'React' + }, + property: { + name: 'createElement' + } + } + }) + .replaceWith(convertNodeToJSX) + .size(); + + if (mutations) { + return root.toSource(printOptions); + } + } + + return null; +}; diff --git a/codemods/legacy/transforms/error-boundaries.js b/codemods/legacy/transforms/error-boundaries.js new file mode 100644 index 0000000..86710eb --- /dev/null +++ b/codemods/legacy/transforms/error-boundaries.js @@ -0,0 +1,12 @@ +module.exports = function(file, api, options) { + const j = api.jscodeshift; + + return j(file.source) + .find(j.Identifier) + .forEach(path => { + if (path.node.name === 'unstable_handleError') { + j(path).replaceWith(j.identifier('componentDidCatch')); + } + }) + .toSource(); +}; diff --git a/codemods/legacy/transforms/findDOMNode.js b/codemods/legacy/transforms/findDOMNode.js new file mode 100644 index 0000000..f8665f9 --- /dev/null +++ b/codemods/legacy/transforms/findDOMNode.js @@ -0,0 +1,151 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +function getDOMNodeToFindDOMNode(file, api, options) { + const j = api.jscodeshift; + + require('./utils/array-polyfills'); + const ReactUtils = require('./utils/ReactUtils')(j); + + const printOptions = options.printOptions || { + quote: 'single', + trailingComma: true + }; + const root = j(file.source); + + const createReactFindDOMNodeCall = arg => + j.callExpression( + j.memberExpression( + j.identifier('React'), + j.identifier('findDOMNode'), + false + ), + [arg] + ); + + const updateRefCall = (path, refName) => { + j(path) + .find(j.CallExpression, { + callee: { + object: { + type: 'Identifier', + name: refName + }, + property: { + type: 'Identifier', + name: 'getDOMNode' + } + } + }) + .forEach(callPath => + j(callPath).replaceWith( + createReactFindDOMNodeCall(j.identifier(refName)) + ) + ); + }; + + const updateToFindDOMNode = classPath => { + var sum = 0; + + // this.getDOMNode() + sum += j(classPath) + .find(j.CallExpression, { + callee: { + object: { + type: 'ThisExpression' + }, + property: { + type: 'Identifier', + name: 'getDOMNode' + } + } + }) + .forEach(path => + j(path).replaceWith(createReactFindDOMNodeCall(j.thisExpression())) + ) + .size(); + + // this.refs.xxx.getDOMNode() or this.refs.xxx.refs.yyy.getDOMNode() + sum += j(classPath) + .find(j.MemberExpression, { + object: { + type: 'MemberExpression', + object: { + type: 'MemberExpression', + object: { + type: 'ThisExpression' + }, + property: { + type: 'Identifier', + name: 'refs' + } + } + } + }) + .closest(j.CallExpression) + .filter( + path => + path.value.callee.property && + path.value.callee.property.type === 'Identifier' && + path.value.callee.property.name === 'getDOMNode' + ) + .forEach(path => + j(path).replaceWith( + createReactFindDOMNodeCall(path.value.callee.object) + ) + ) + .size(); + + // someVariable.getDOMNode() wherre `someVariable = this.refs.xxx` + sum += j(classPath) + .findVariableDeclarators() + .filter(path => { + const init = path.value.init; + const value = init && init.object; + return ( + value && + value.type === 'MemberExpression' && + value.object && + value.object.type === 'ThisExpression' && + value.property && + value.property.type === 'Identifier' && + value.property.name === 'refs' && + init.property && + init.property.type === 'Identifier' + ); + }) + .forEach(path => + j(path) + .closest(j.FunctionExpression) + .forEach(fnPath => updateRefCall(fnPath, path.value.id.name)) + ) + .size(); + + return sum > 0; + }; + + if (options['explicit-require'] === false || ReactUtils.hasReact(root)) { + const apply = path => path.filter(updateToFindDOMNode); + + const didTransform = + apply(ReactUtils.findReactCreateClass(root)).size() + + apply(ReactUtils.findReactCreateClassModuleExports(root)).size() + + apply(ReactUtils.findReactCreateClassExportDefault(root)).size() > + 0; + + if (didTransform) { + return root.toSource(printOptions); + } + } + + return null; +} + +module.exports = getDOMNodeToFindDOMNode; diff --git a/codemods/legacy/transforms/manual-bind-to-arrow.js b/codemods/legacy/transforms/manual-bind-to-arrow.js new file mode 100644 index 0000000..7afb09a --- /dev/null +++ b/codemods/legacy/transforms/manual-bind-to-arrow.js @@ -0,0 +1,184 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/** + * class Component extends React.Component { + * constructor() { this.onClick = this.onClick.bind(this); } + * onClick() { } + * } + * + * --> + * + * class Component extends React.Component { + * onClick = () => { } + * } + */ + +export default function transformer(file, api, options) { + const j = api.jscodeshift; + const doesNotUseArguments = require('./utils/doesNotUseArguments')(j); + + const printOptions = options.printOptions || {}; + var root = j(file.source); + + // Helper functions to transform a method declaration to an arrow function + // By default recast drops comments and jscodeshift doesn't have a way to + // set the return type in the convenience method. Otherwise we would have + // inlined all those. + function withComments(to, from) { + to.comments = from.comments; + return to; + } + + function createArrowFunctionExpression(fn) { + var arrowFunc = j.arrowFunctionExpression(fn.params, fn.body, false); + + arrowFunc.returnType = fn.returnType; + arrowFunc.defaults = fn.defaults; + arrowFunc.rest = fn.rest; + arrowFunc.async = fn.async; + + return arrowFunc; + } + + function createArrowProperty(prop) { + return withComments( + j.classProperty( + j.identifier(prop.key.name), + createArrowFunctionExpression(prop.value), + null, + false + ), + prop + ); + } + + var hasChanged = false; + var transform = root.find(j.AssignmentExpression).forEach(path => { + // Check that the englobing function is constructor + var methodPath = path; + while ( + methodPath && + (methodPath.node.type !== 'MethodDefinition' || + methodPath.node.kind !== 'constructor') + ) { + methodPath = methodPath.parentPath; + } + if (!methodPath) { + return; + } + + // Check that it looks like + // this.method = this.method.bind(this); + // or + // (this: any).method = this.method.bind(this); + // or + // self.method = this.method.bind(this); + if ( + !( + path.node.left.type === 'MemberExpression' && + // this + (path.node.left.object.type === 'ThisExpression' || + // self + (path.node.left.object.type === 'Identifier' && + path.node.left.object.name === 'self') || + // (this: any) + (path.node.left.object.type === 'TypeCastExpression' && + path.node.left.object.expression.type === 'ThisExpression')) && + path.node.left.property.type === 'Identifier' && + path.node.right.type === 'CallExpression' && + path.node.right.callee.type === 'MemberExpression' && + path.node.right.callee.property.type === 'Identifier' && + path.node.right.callee.property.name === 'bind' && + path.node.right.callee.object.type === 'MemberExpression' && + path.node.right.callee.object.property.type === 'Identifier' && + path.node.right.callee.object.object.type === 'ThisExpression' && + path.node.left.property.name === + path.node.right.callee.object.property.name && + true + ) + ) { + return; + } + + // Find the method() declaration and replace it with an arrow function + var methodName = path.node.left.property.name; + + const componentDecl = methodPath.parentPath; + var methods = j(componentDecl) + .find(j.MethodDefinition) + .filter( + path => + path.node.key.type === 'Identifier' && + path.node.key.name === methodName && + doesNotUseArguments(path, file.path) + ); + + // Do not remove the binding if there's no corresponding method to turn + // into an arrow function, or if the method uses `arguments` keyword inside + // it. + if (methods.size() === 0) { + return; + } + methods.replaceWith(path => createArrowProperty(path.node)); + + // Remove the line + // this.method = this.method.bind(this); + j(path.parentPath).remove(); + + var selfCount = j(methodPath) + .find(j.Identifier, { name: 'self' }) + .size(); + if (selfCount === 1) { + // Remove the line + // const self: any = this; + // If self is present somewhere else in the method, then it is + // not safe to do. + j(methodPath) + .find(j.VariableDeclaration) + .filter( + path => + j(path) + .find(j.Identifier, { name: 'self' }) + .size() === 1 + ) + .remove(); + } + + // If we delete everything from the constructor but the super() call, + // then delete the entire constructor. + var canDeleteConstructor = true; + methodPath.node.value.body.body.forEach(node => { + if ( + !node || + (node.type === 'ExpressionStatement' && + node.expression.type === 'CallExpression' && + // babylon parser + (node.expression.callee.type === 'Super' || + // flow parser + (node.expression.callee.type === 'Identifier' && + node.expression.callee.name === 'super'))) + ) { + return; + } + canDeleteConstructor = false; + }); + if (canDeleteConstructor) { + j(methodPath).remove(); + } + + hasChanged++; + }); + + if (hasChanged) { + return transform.toSource(printOptions); + } + return null; +} + +// module.exports.parser = 'flow'; \ No newline at end of file diff --git a/codemods/legacy/transforms/pure-component.js b/codemods/legacy/transforms/pure-component.js new file mode 100644 index 0000000..af538ad --- /dev/null +++ b/codemods/legacy/transforms/pure-component.js @@ -0,0 +1,339 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +module.exports = function(file, api, options) { + const j = api.jscodeshift; + const ReactUtils = require('./utils/ReactUtils')(j); + + const useArrows = options.useArrows || false; + const destructuringEnabled = options.destructuring || false; + const silenceWarnings = options.silenceWarnings || false; + const printOptions = options.printOptions || { + quote: 'single', + trailingComma: true + }; + + const getClassName = path => path.node.id.name; + + const isRenderMethod = node => + node.type == 'MethodDefinition' && + node.key.type == 'Identifier' && + node.key.name == 'render'; + + const isPropsProperty = node => + node.type === 'ClassProperty' && + node.key.type === 'Identifier' && + node.key.name === 'props'; + + const isStaticProperty = node => node.type === 'ClassProperty' && node.static; + + const onlyHasRenderMethod = path => + j(path) + .find(j.MethodDefinition) + .filter(p => !isRenderMethod(p.value)) + .size() === 0; + + const onlyHasSafeClassProperties = path => + j(path) + .find(j.ClassProperty) + .filter(p => !(isPropsProperty(p.value) || isStaticProperty(p.value))) + .size() === 0; + + const hasRefs = path => + j(path) + .find(j.JSXAttribute, { + name: { + type: 'JSXIdentifier', + name: 'ref' + } + }) + .size() > 0; + + const THIS_PROPS = { + object: { + type: 'ThisExpression' + }, + property: { + name: 'props' + } + }; + + const replaceThisProps = path => + j(path) + .find(j.MemberExpression, THIS_PROPS) + .replaceWith(j.identifier('props')); + + const buildIdentifierWithTypeAnnotation = (name, typeAnnotation) => { + const identifier = j.identifier(name); + if (typeAnnotation) { + identifier.typeAnnotation = j.typeAnnotation(typeAnnotation); + } + return identifier; + }; + + const isDuplicateDeclaration = (path, pre) => { + if (path && path.value && path.value.id && path.value.init) { + const initName = pre + ? path.value.init.property && path.value.init.property.name + : path.value.init.name; + return path.value.id.name === initName; + } + return false; + }; + + const needsThisDotProps = path => + path + .find(j.Identifier, { + name: 'props' + }) + .filter(p => p.parentPath.parentPath.value.type !== 'MemberExpression') + .size() > 0; + + const getPropNames = path => { + const propNames = new Set(); + path + .find(j.MemberExpression, { + object: { + property: { + name: 'props' + } + } + }) + .forEach(p => { + propNames.add(p.value.property.name); + }); + return propNames; + }; + + const getDuplicateNames = path => { + const duplicates = new Set(); + path + .find(j.VariableDeclarator) + .filter(p => isDuplicateDeclaration(p, true)) + .forEach(p => { + duplicates.add(p.value.id.name); + }); + return duplicates; + }; + + const getAssignmentNames = path => { + const assignmentNames = new Set(); + path + .find(j.Identifier) + .filter(p => { + if (p.value.type === 'JSXIdentifier') { + return false; + } + if ( + !(p.parentPath.value.object && p.parentPath.value.object.property) + ) { + return true; + } + return p.parentPath.value.object.property.name !== 'props'; + }) + .forEach(p => { + assignmentNames.add(p.value.name); + }); + return assignmentNames; + }; + + const hasAssignmentsThatShadowProps = path => { + const propNames = getPropNames(path); + const assignmentNames = getAssignmentNames(path); + const duplicates = getDuplicateNames(path); + return Array.from(propNames).some( + prop => !duplicates.has(prop) && assignmentNames.has(prop) + ); + }; + + const canDestructure = path => + !needsThisDotProps(path) && !hasAssignmentsThatShadowProps(path); + + const createShorthandProperty = (j, typeAnnotation) => prop => { + const property = j.property('init', j.identifier(prop), j.identifier(prop)); + property.shorthand = true; + if (typeAnnotation) { + typeAnnotation.properties.forEach(t => { + if (t.key.name === prop) { + property.key.typeAnnotation = j.typeAnnotation(t.value); + } + }); + } + return property; + }; + + const destructureProps = (body, typeAnnotation) => { + const toDestructure = body.find(j.MemberExpression, { + object: { + name: 'props' + } + }); + if (toDestructure) { + const propNames = new Set(); + toDestructure.replaceWith(path => { + const propName = path.value.property.name; + propNames.add(propName); + return j.identifier(propName); + }); + if (propNames.size > 0) { + const assignments = body.find(j.VariableDeclarator); + const duplicateAssignments = assignments.filter(a => + isDuplicateDeclaration(a, false) + ); + duplicateAssignments.remove(); + return j.objectPattern( + Array.from(propNames).map(createShorthandProperty(j, typeAnnotation)) + ); + } + } + return false; + }; + + const findPropsTypeAnnotation = body => { + const property = body.find(isPropsProperty); + + return property && property.typeAnnotation.typeAnnotation; + }; + + const isDefaultExport = path => + path.parentPath && path.parentPath.value.type === 'ExportDefaultDeclaration'; + + const safelyDefaultExportDeclaration = (path) => { + const localName = path.value.declarations[0].id.name; + j(path.parent) + .replaceWith(_ => path.value) + .insertAfter( + j.exportDeclaration(true, { type: 'Identifier', name: localName }) + ); + }; + + const build = useArrows => (name, body, typeAnnotation, destructure, hasThisDotProps) => { + const identifier = j.identifier(name); + const propsIdentifier = buildIdentifierWithTypeAnnotation( + 'props', + typeAnnotation + ); + + const propsArg = hasThisDotProps ? [ + (destructure && destructureProps(j(body), typeAnnotation)) || + propsIdentifier + ] : []; + if (useArrows) { + return j.variableDeclaration('const', [ + j.variableDeclarator( + identifier, + j.arrowFunctionExpression(propsArg, body) + ) + ]); + } + return j.functionDeclaration(identifier, propsArg, body); + }; + + const buildPureComponentFunction = build(); + + const buildPureComponentArrowFunction = build(true); + + const buildStatics = (name, properties) => + properties.map(prop => + j.expressionStatement( + j.assignmentExpression( + '=', + j.memberExpression(j.identifier(name), prop.key), + prop.value + ) + ) + ); + + const reportSkipped = path => { + const name = getClassName(path); + const fileName = file.path; + if (!path.value.loc) { + console.warn(`Class "${name}" skipped in ${fileName}`); + return; + } + const { line, column } = path.value.loc.start; + + console.warn(`Class "${name}" skipped in ${fileName} on ${line}:${column}`); + }; + + const f = j(file.source); + + const pureClasses = ReactUtils.findReactES6ClassDeclaration(f).filter( + path => { + const isPure = + onlyHasRenderMethod(path) && + onlyHasSafeClassProperties(path) && + !hasRefs(path); + if (!isPure && !silenceWarnings) { + reportSkipped(path); + } + + return isPure; + } + ); + + if (pureClasses.size() === 0) { + return null; + } + + // Save the names of the deleted pure classes super class + // We need this to prune unused variables at the end. + const parentClassNames = pureClasses.nodes().map(node => node.superClass.name); + + pureClasses.replaceWith(p => { + const name = p.node.id.name; + const renderMethod = p.value.body.body.filter(isRenderMethod)[0]; + const renderBody = renderMethod.value.body; + const propsTypeAnnotation = findPropsTypeAnnotation(p.value.body.body); + const statics = p.value.body.body.filter(isStaticProperty); + const destructure = destructuringEnabled && canDestructure(j(renderMethod)); + + if (destructuringEnabled && !destructure) { + console.warn(`Unable to destructure ${name} props.`); + } + + const hasThisDotProps = j(renderBody).find(j.MemberExpression, THIS_PROPS).length > 0; + replaceThisProps(renderBody); + + if (useArrows) { + return [ + buildPureComponentArrowFunction( + name, + renderBody, + propsTypeAnnotation, + destructure, + hasThisDotProps + ), + ...buildStatics(name, statics) + ]; + } else { + return [ + buildPureComponentFunction( + name, + renderBody, + propsTypeAnnotation, + destructure, + hasThisDotProps + ), + ...buildStatics(name, statics) + ]; + } + }).forEach(p => { + // Check for combining default keyword with const declaration + if (useArrows && isDefaultExport(p)) { + safelyDefaultExportDeclaration(p); + } + }).forEach((p, i) => { + const parentClassName = parentClassNames[i]; + ReactUtils.removeUnusedSuperClassImport(j(p), f, parentClassName); + }); + + return f.toSource(printOptions); +}; diff --git a/codemods/legacy/transforms/pure-render-mixin.js b/codemods/legacy/transforms/pure-render-mixin.js new file mode 100644 index 0000000..00b2b72 --- /dev/null +++ b/codemods/legacy/transforms/pure-render-mixin.js @@ -0,0 +1,179 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +function removePureRenderMixin(file, api, options) { + const j = api.jscodeshift; + + require('./utils/array-polyfills'); + const ReactUtils = require('./utils/ReactUtils')(j); + + const printOptions = options.printOptions || { + quote: 'single', + trailingComma: true + }; + const root = j(file.source); + + const PURE_RENDER_MIXIN = options['mixin-name'] || 'PureRenderMixin'; + const SHOULD_COMPONENT_UPDATE = 'shouldComponentUpdate'; + const NEXT_PROPS = 'nextProps'; + const NEXT_STATE = 'nextState'; + + // --------------------------------------------------------------------------- + // shouldComponentUpdate + const createShouldComponentUpdateFunction = () => + j.functionExpression( + null, + [j.identifier(NEXT_PROPS), j.identifier(NEXT_STATE)], + j.blockStatement([ + j.returnStatement( + j.callExpression( + j.memberExpression( + j.identifier('React'), + j.memberExpression( + j.identifier('addons'), + j.identifier('shallowCompare'), + false + ), + false + ), + [ + j.thisExpression(), + j.identifier(NEXT_PROPS), + j.identifier(NEXT_STATE) + ] + ) + ) + ]) + ); + + const createShouldComponentUpdateProperty = () => + j.property( + 'init', + j.identifier(SHOULD_COMPONENT_UPDATE), + createShouldComponentUpdateFunction() + ); + + const hasShouldComponentUpdate = classPath => + ReactUtils.getReactCreateClassSpec(classPath).properties.every( + property => property.key.name !== SHOULD_COMPONENT_UPDATE + ); + + // --------------------------------------------------------------------------- + // Mixin related code + const isPureRenderMixin = node => + node.type === 'Identifier' && node.name === PURE_RENDER_MIXIN; + + const hasPureRenderMixin = classPath => { + const spec = ReactUtils.getReactCreateClassSpec(classPath); + const mixin = spec && spec.properties.find(ReactUtils.isMixinProperty); + return mixin && mixin.value.elements.some(isPureRenderMixin); + }; + + const removeMixin = elements => + j.property( + 'init', + j.identifier('mixins'), + j.arrayExpression(elements.filter(element => !isPureRenderMixin(element))) + ); + + // --------------------------------------------------------------------------- + // Boom! + const insertShouldComponentUpdate = properties => { + const length = properties.length; + const lastProp = properties[length - 1]; + // I wouldn't dare insert at the bottom if the last function is render + if (lastProp.key.type === 'Identifier' && lastProp.key.name === 'render') { + properties.splice( + length - 1, + 1, + createShouldComponentUpdateProperty(), + lastProp + ); + } else { + properties.push(createShouldComponentUpdateProperty()); + } + return properties; + }; + + const cleanupReactComponent = classPath => { + const spec = ReactUtils.getReactCreateClassSpec(classPath); + const properties = spec.properties + .map(property => { + if (ReactUtils.isMixinProperty(property)) { + const elements = property.value.elements; + return elements.length !== 1 ? removeMixin(elements) : null; + } + return property; + }) + .filter(property => !!property); + + ReactUtils.findReactCreateClassCallExpression(classPath).replaceWith( + ReactUtils.createCreateReactClassCallExpression( + insertShouldComponentUpdate(properties) + ) + ); + }; + + // Remove it if only two or fewer are left: + // var PureRenderMixin = React.addons.PureRenderMixin; + const hasPureRenderIdentifiers = path => + path + .find(j.Identifier, { + name: PURE_RENDER_MIXIN + }) + .size() > 2; + + const deletePureRenderMixin = path => { + if (hasPureRenderIdentifiers(path)) { + return; + } + + const declaration = path + .findVariableDeclarators(PURE_RENDER_MIXIN) + .closest(j.VariableDeclaration); + + if (declaration.size > 1) { + declaration.forEach(p => + j(p).replaceWith( + j.variableDeclaration( + 'var', + p.value.declarations.filter(isPureRenderMixin) + ) + ) + ); + } else { + // Let's assume the variable declaration happens at the top level + const program = declaration.closest(j.Program).get(); + const body = program.value.body; + const index = body.indexOf(declaration.get().value); + if (index !== -1) { + body.splice(index, 1); + } + } + }; + + if (options['explicit-require'] === false || ReactUtils.hasReact(root)) { + const didTransform = + ReactUtils.findReactCreateClass(root) + .filter(hasPureRenderMixin) + .filter(hasShouldComponentUpdate) + .forEach(cleanupReactComponent) + .size() > 0; + + if (didTransform) { + deletePureRenderMixin(root); + return root.toSource(printOptions); + } + } + + return null; +} + +module.exports = removePureRenderMixin; diff --git a/codemods/legacy/transforms/react-to-react-dom.js b/codemods/legacy/transforms/react-to-react-dom.js new file mode 100644 index 0000000..e476adb --- /dev/null +++ b/codemods/legacy/transforms/react-to-react-dom.js @@ -0,0 +1,421 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +var CORE_PROPERTIES = [ + 'Children', + 'Component', + 'createElement', + 'cloneElement', + 'isValidElement', + 'PropTypes', + 'createClass', + 'createFactory', + 'createMixin', + 'DOM', + '__spread' +]; + +var DOM_PROPERTIES = [ + 'findDOMNode', + 'render', + 'unmountComponentAtNode', + 'unstable_batchedUpdates', + 'unstable_renderSubtreeIntoContainer' +]; + +var DOM_SERVER_PROPERTIES = ['renderToString', 'renderToStaticMarkup']; + +function reportError(node, error) { + throw new Error( + `At ${node.loc.start.line}:${node.loc.start.column}: ${error}` + ); +} + +function isRequire(path, moduleName) { + return ( + path.value.type === 'CallExpression' && + path.value.callee.type === 'Identifier' && + path.value.callee.name === 'require' && + path.value.arguments.length === 1 && + path.value.arguments[0].type === 'Literal' && + path.value.arguments[0].value === moduleName + ); +} + +module.exports = function(file, api, options) { + var j = api.jscodeshift; + + var printOptions = options.printOptions || { quote: 'single' }; + var root = j(file.source); + + [ + ['React', 'ReactDOM', 'ReactDOMServer'], + ['react', 'react-dom', 'react-dom/server'] + ].forEach(function(pair) { + var coreModuleName = pair[0]; + var domModuleName = pair[1]; + var domServerModuleName = pair[2]; + + var domAlreadyDeclared = false; + var domServerAlreadyDeclared = false; + + var coreRequireDeclarator; + var coreImportDeclaration; + var coreName = null; + root + .find(j.CallExpression) + .filter(p => isRequire(p, coreModuleName)) + .forEach(p => { + var name, scope; + if (p.parent.value.type === 'VariableDeclarator') { + if (p.parent.value.id.type === 'ObjectPattern') { + var pattern = p.parent.value.id; + var all = pattern.properties.every(function(prop) { + if (prop.key.type === 'Identifier') { + name = prop.key.name; + return CORE_PROPERTIES.indexOf(name) !== -1; + } + return false; + }); + if (all) { + // var {PropTypes} = require('React'); so leave alone + return; + } + } + if (coreRequireDeclarator) { + reportError(p.value, 'Multiple declarations of React'); + } + if (p.parent.value.id.type !== 'Identifier') { + reportError( + p.value, + 'Unexpected destructuring in require of ' + coreModuleName + ); + } + name = p.parent.value.id.name; + scope = p.scope.lookup(name); + if (scope.declares('ReactDOM')) { + console.log('Using existing ReactDOM var in ' + file.path); + domAlreadyDeclared = true; + } + if (scope.declares('ReactDOMServer')) { + console.log('Using existing ReactDOMServer var in ' + file.path); + domServerAlreadyDeclared = true; + } + coreRequireDeclarator = p.parent; + coreName = coreRequireDeclarator.value.id.name; + } else if (p.parent.value.type === 'AssignmentExpression') { + if (p.parent.value.left.type !== 'Identifier') { + reportError( + p.value, + 'Unexpected destructuring in require of ' + coreModuleName + ); + } + name = p.parent.value.left.name; + scope = p.scope.lookup(name); + var reactBindings = scope.getBindings()[name]; + if (reactBindings.length !== 1) { + throw new Error( + 'Unexpected number of bindings: ' + reactBindings.length + ); + } + coreRequireDeclarator = reactBindings[0].parent; + coreName = coreRequireDeclarator.value.id.name; + if ( + coreRequireDeclarator.value.init && + !isRequire(coreRequireDeclarator.get('init'), coreModuleName) + ) { + reportError( + coreRequireDeclarator.value, + 'Unexpected initialization of ' + coreModuleName + ); + } + if (scope.declares('ReactDOM')) { + console.log('Using existing ReactDOM var in ' + file.path); + domAlreadyDeclared = true; + } + if (scope.declares('ReactDOMServer')) { + console.log('Using existing ReactDOMServer var in ' + file.path); + domServerAlreadyDeclared = true; + } + } + }); + root + .find(j.ImportDeclaration, { source: { value: coreModuleName } }) + .forEach(p => { + if (coreImportDeclaration) { + reportError(p.value, 'Multiple declarations of React'); + } + coreImportDeclaration = p; + var defaultSpecifier = p.value.specifiers.find( + sp => sp.type === j.ImportDefaultSpecifier.name + ); + if (defaultSpecifier) { + var name = defaultSpecifier.local.name; + var scope = p.scope.lookup(name); + if (scope.declares('ReactDOM')) { + console.log('Using existing ReactDOM var in ' + file.path); + domAlreadyDeclared = true; + } + if (scope.declares('ReactDOMServer')) { + console.log('Using existing ReactDOMServer var in ' + file.path); + domServerAlreadyDeclared = true; + } + coreName = j(coreImportDeclaration) + .find(j.ImportDefaultSpecifier) + .get().value.local.name; + } + }); + + if (!coreRequireDeclarator && !coreImportDeclaration) { + return; + } + + if ( + !domAlreadyDeclared && + root.find(j.Identifier, { name: 'ReactDOM' }).size() > 0 + ) { + throw new Error( + 'ReactDOM is already defined in a different scope than React' + ); + } + if ( + !domServerAlreadyDeclared && + root.find(j.Identifier, { name: 'ReactDOMServer' }).size() > 0 + ) { + throw new Error( + 'ReactDOMServer is already defined in a different scope than React' + ); + } + + var processed = new Set(); + var requireAssignments = []; + var coreUses = 0; + var domUses = 0; + var domServerUses = 0; + + root.find(j.Identifier, { name: coreName }).forEach(p => { + if (processed.has(p.value)) { + // https://github.com/facebook/jscodeshift/issues/36 + return; + } + processed.add(p.value); + if ( + p.parent.value.type === 'MemberExpression' || + p.parent.value.type === 'QualifiedTypeIdentifier' + ) { + var left; + var right; + if (p.parent.value.type === 'MemberExpression') { + left = p.parent.value.object; + right = p.parent.value.property; + } else { + left = p.parent.value.qualification; + right = p.parent.value.id; + } + if (left === p.value) { + // React.foo (or React[foo]) + if (right.type === 'Identifier') { + var name = right.name; + if (CORE_PROPERTIES.indexOf(name) !== -1) { + coreUses++; + } else if (DOM_PROPERTIES.indexOf(name) !== -1) { + domUses++; + j(p).replaceWith(j.identifier('ReactDOM')); + } else if (DOM_SERVER_PROPERTIES.indexOf(name) !== -1) { + domServerUses++; + j(p).replaceWith(j.identifier('ReactDOMServer')); + } else { + throw new Error('Unknown property React.' + name); + } + } + } else if (right === p.value) { + // foo.React, no need to transform + } else { + throw new Error('unimplemented'); + } + } else if (p.parent.value.type === 'VariableDeclarator') { + if (p.parent.value.id === p.value) { + // var React = ...; + } else if (p.parent.value.init === p.value) { + // var ... = React; + var pattern = p.parent.value.id; + if (pattern.type === 'ObjectPattern') { + // var {PropTypes} = React; + // Most of these cases will just be looking at {PropTypes} so this + // is usually a no-op. + var coreProperties = []; + var domProperties = []; + pattern.properties.forEach(function(prop) { + if (prop.key.type === 'Identifier') { + var key = prop.key.name; + if (CORE_PROPERTIES.indexOf(key) !== -1) { + coreProperties.push(prop); + } else if (DOM_PROPERTIES.indexOf(key) !== -1) { + domProperties.push(prop); + } else { + throw new Error( + 'Unknown property React.' + key + ' while destructuring' + ); + } + } else { + throw new Error('unimplemented'); + } + }); + var domDeclarator = j.variableDeclarator( + j.objectPattern(domProperties), + j.identifier('ReactDOM') + ); + if (coreProperties.length && !domProperties.length) { + // nothing to do + coreUses++; + } else if (domProperties.length && !coreProperties.length) { + domUses++; + j(p.parent).replaceWith(domDeclarator); + } else { + coreUses++; + domUses++; + var decl = j(p).closest(j.VariableDeclaration); + decl.insertAfter( + j.variableDeclaration(decl.get().value.kind, [domDeclarator]) + ); + } + } else { + throw new Error('unimplemented'); + } + } else { + throw new Error('unimplemented'); + } + } else if (p.parent.value.type === 'AssignmentExpression') { + if (p.parent.value.left === p.value) { + if (isRequire(p.parent.get('right'), coreModuleName)) { + requireAssignments.push(p.parent); + } else { + reportError( + p.parent.value, + 'Unexpected assignment to ' + coreModuleName + ); + } + } else { + throw new Error('unimplemented'); + } + } else if (p.parent.value.type === 'ImportDefaultSpecifier') { + // import React from "react"; + } else { + reportError(p.value, 'unimplemented ' + p.parent.value.type); + } + }); + + coreUses += root.find(j.JSXElement).size(); + + function insertRequire(name, path) { + var req = j.callExpression(j.identifier('require'), [j.literal(path)]); + requireAssignments.forEach(function(requireAssignment) { + requireAssignment.parent.insertAfter( + j.expressionStatement( + j.assignmentExpression('=', j.identifier(name), req) + ) + ); + }); + coreRequireDeclarator.parent.insertAfter( + j.variableDeclaration(coreRequireDeclarator.parent.value.kind, [ + j.variableDeclarator( + j.identifier(name), + coreRequireDeclarator.value.init ? req : null + ) + ]) + ); + } + + if (coreRequireDeclarator) { + if (domServerUses > 0 && !domServerAlreadyDeclared) { + insertRequire('ReactDOMServer', domServerModuleName); + } + if (domUses > 0 && !domAlreadyDeclared) { + insertRequire('ReactDOM', domModuleName); + } + if ((domUses > 0 || domServerUses > 0) && coreUses === 0) { + j(coreRequireDeclarator).remove(); + requireAssignments.forEach(r => j(r).remove()); + } + } else { + function findImportPath(name, path) { + return root + .find(j.ImportDeclaration, { source: { value: path } }) + .filter(p => + p.value.specifiers.find( + sp => + sp.type === 'ImportDefaultSpecifier' && sp.local.name === name + ) + ); + } + + function emitImport(name, path, knownProperties, uses) { + const usedProperties = coreImportDeclaration.value.specifiers + .filter(sp => sp.type === j.ImportSpecifier.name) + .filter( + sp => + sp.imported.type === 'Identifier' && + knownProperties.indexOf(sp.imported.name) !== -1 + ) + .map(sp => sp.imported.name); + + const importDeclaration = findImportPath(name, path); + const specifiers = []; + + // if ReactDOM needs to be in scope and it's not declared, or we're going to + // replace its declaration to add import specifiers... + if (uses > 0 && (!domAlreadyDeclared || importDeclaration.length > 0)) { + specifiers.push(j.importDefaultSpecifier(j.identifier(name))); + } + if (usedProperties.length > 0) { + j(coreImportDeclaration) + .find(j.ImportSpecifier) + .filter(p => usedProperties.indexOf(p.value.local.name) !== -1) + .remove(); + specifiers.push( + ...usedProperties.map(prop => j.importSpecifier(j.identifier(prop))) + ); + } + if (specifiers.length > 0) { + if (importDeclaration.length > 0) { + importDeclaration.replaceWith( + j.importDeclaration(specifiers, j.literal(path)) + ); + } else { + coreImportDeclaration.insertAfter( + j.importDeclaration(specifiers, j.literal(path)) + ); + } + } + } + + emitImport('ReactDOM', domModuleName, DOM_PROPERTIES, domUses); + emitImport( + 'ReactDOMServer', + domServerModuleName, + DOM_SERVER_PROPERTIES, + domServerUses + ); + + const coreImportSpecifiers = j(coreImportDeclaration).find( + j.ImportSpecifier + ); + if (coreImportSpecifiers.length === 0 && coreUses === 0) { + j(coreImportDeclaration).remove(); + } else if (coreImportSpecifiers.length > 0 && coreUses === 0) { + j(coreImportDeclaration) + .find(j.ImportDefaultSpecifier) + .remove(); + } + } + }); + + return root.toSource(printOptions); +}; diff --git a/codemods/legacy/transforms/remove-context-provider.ts b/codemods/legacy/transforms/remove-context-provider.ts new file mode 100644 index 0000000..4aadfbe --- /dev/null +++ b/codemods/legacy/transforms/remove-context-provider.ts @@ -0,0 +1,40 @@ +import type { API, FileInfo } from 'jscodeshift'; + +export default function transform( + file: FileInfo, + api: API, +): string | undefined { + const j = api.jscodeshift; + const root = j(file.source); + + let isDirty = false; + + root.findJSXElements().forEach((elementPath) => { + const { value } = elementPath; + const elements = [value.openingElement, value.closingElement]; + elements.forEach((element) => { + if (!element) { + return; + } + if ( + !j.JSXMemberExpression.check(element.name) || + !j.JSXIdentifier.check(element.name.object) + ) { + return; + } + + const objectName = element.name.object.name; + const propertyName = element.name.property.name; + + if ( + objectName.toLocaleLowerCase().includes('context') && + propertyName === 'Provider' + ) { + element.name = element.name.object; + isDirty = true; + } + }); + }); + + return isDirty ? root.toSource() : undefined; +} diff --git a/codemods/legacy/transforms/remove-forward-ref.ts b/codemods/legacy/transforms/remove-forward-ref.ts new file mode 100644 index 0000000..da7ad9d --- /dev/null +++ b/codemods/legacy/transforms/remove-forward-ref.ts @@ -0,0 +1,297 @@ +import type { + API, + ArrowFunctionExpression, + CallExpression, + FileInfo, + FunctionExpression, + Identifier, + JSCodeshift, + TSTypeLiteral, + TSTypeReference, +} from 'jscodeshift'; + +// Props & { ref: React.RefObject} +const buildPropsAndRefIntersectionTypeAnnotation = ( + j: JSCodeshift, + propType: TSTypeReference | TSTypeLiteral, + refType: TSTypeReference | TSTypeLiteral | null, +) => + j.tsTypeAnnotation( + j.tsIntersectionType([ + propType, + j.tsTypeLiteral([ + j.tsPropertySignature.from({ + key: j.identifier('ref'), + typeAnnotation: j.tsTypeAnnotation( + j.tsTypeReference.from({ + typeName: j.tsQualifiedName( + j.identifier('React'), + j.identifier('RefObject'), + ), + typeParameters: j.tsTypeParameterInstantiation([ + refType === null ? j.tsUnknownKeyword() : refType, + ]), + }), + ), + }), + ]), + ]), + ); + +// { ref: refName, ...propsName } +const buildRefAndPropsObjectPattern = ( + j: JSCodeshift, + refArgName: string, + propArgName: string, +) => + j.objectPattern([ + j.objectProperty.from({ + shorthand: true, + key: j.identifier('ref'), + value: j.identifier(refArgName), + }), + j.restProperty(j.identifier(propArgName)), + ]); + +// React.ForwardedRef => HTMLButtonElement +const getRefTypeFromRefArg = (j: JSCodeshift, refArg: Identifier) => { + const typeReference = refArg.typeAnnotation?.typeAnnotation; + if ( + !j.TSTypeReference.check(typeReference) || + !j.TSQualifiedName.check(typeReference.typeName) + ) { + return null; + } + + const { right } = typeReference.typeName; + + if (!j.Identifier.check(right) || right.name === 'forwardedRef') { + return null; + } + + const [firstTypeParameter] = typeReference.typeParameters?.params ?? []; + + if (!j.TSTypeReference.check(firstTypeParameter)) { + return null; + } + + return firstTypeParameter; +}; + +const getForwardRefRenderFunction = ( + j: JSCodeshift, + callExpression: CallExpression, +): FunctionExpression | ArrowFunctionExpression | null => { + const [renderFunction] = callExpression.arguments; + + if ( + !j.FunctionExpression.check(renderFunction) && + !j.ArrowFunctionExpression.check(renderFunction) + ) { + return null; + } + + return renderFunction; +}; + +const isLiteralOrReference = ( + j: JSCodeshift, + type: unknown, +): type is TSTypeReference | TSTypeLiteral => { + return j.TSTypeReference.check(type) || j.TSTypeLiteral.check(type); +}; + +export default function transform(file: FileInfo, api: API) { + const j = api.jscodeshift; + + const root = j(file.source); + + let isDirty = false; + + let reactForwardRefImportLocalName: string | null = null; + let reactDefaultImportName: string | null = null; + + root + .find(j.ImportDeclaration, { + source: { value: 'react' }, + }) + .forEach((path) => { + path.value.specifiers?.forEach((specifier) => { + // named import + if ( + j.ImportSpecifier.check(specifier) && + specifier.imported.name === 'forwardRef' + ) { + reactForwardRefImportLocalName = specifier.local?.name ?? null; + } + + // default and wildcard import + if ( + j.ImportDefaultSpecifier.check(specifier) || + j.ImportNamespaceSpecifier.check(specifier) + ) { + reactDefaultImportName = specifier.local?.name ?? null; + } + }); + }); + + root + .find(j.CallExpression) + .filter((path) => { + const { callee } = path.value; + + if ( + j.Identifier.check(callee) && + callee.name === reactForwardRefImportLocalName + ) { + return true; + } + + if ( + j.MemberExpression.check(callee) && + j.Identifier.check(callee.object) && + callee.object.name === reactDefaultImportName && + j.Identifier.check(callee.property) && + callee.property.name === 'forwardRef' + ) { + return true; + } + + return false; + }) + .replaceWith((callExpressionPath) => { + const originalCallExpression = callExpressionPath.value; + + const renderFunction = getForwardRefRenderFunction( + j, + callExpressionPath.node, + ); + + if (renderFunction === null) { + console.warn('Could not detect render function.'); + + return originalCallExpression; + } + + const [propsArg, refArg] = renderFunction.params; + + if ( + !j.Identifier.check(refArg) || + !(j.Identifier.check(propsArg) || j.ObjectPattern.check(propsArg)) + ) { + console.warn('Could not detect ref or props arguments.'); + + return originalCallExpression; + } + + const refArgTypeReference = getRefTypeFromRefArg(j, refArg); + const refArgName = refArg.name; + + const propsArgTypeReference = propsArg.typeAnnotation?.typeAnnotation; + // remove refArg + renderFunction.params.splice(1, 1); + + // if propsArg is ObjectPattern, add ref as new ObjectProperty + if (j.ObjectPattern.check(propsArg)) { + propsArg.properties.unshift( + j.objectProperty.from({ + shorthand: true, + key: j.identifier('ref'), + value: j.identifier(refArgName), + }), + ); + + isDirty = true; + } + + // if props arg is Identifier, push ref variable declaration to the function body + if (j.Identifier.check(propsArg)) { + renderFunction.params[0] = buildRefAndPropsObjectPattern( + j, + refArg.name, + propsArg.name, + ); + + isDirty = true; + } + + /** + * Transform ts types: render function props and ref are typed + */ + + if ( + isLiteralOrReference(j, propsArgTypeReference) && + renderFunction.params?.[0] && + 'typeAnnotation' in renderFunction.params[0] + ) { + renderFunction.params[0].typeAnnotation = + buildPropsAndRefIntersectionTypeAnnotation( + j, + propsArgTypeReference, + refArgTypeReference, + ); + isDirty = true; + } + + /** + * Transform ts types: forwardRef type arguments are used + */ + + const typeParameters = callExpressionPath.node.typeParameters; + + // if typeParameters are used in forwardRef generic, reuse them to annotate props type + // forwardRef((props) => { ... }) ====> (props: Props & { ref: React.RefObject }) => { ... } + if ( + j.TSTypeParameterInstantiation.check(typeParameters) && + renderFunction.params?.[0] && + 'typeAnnotation' in renderFunction.params[0] + ) { + const [refType, propType] = typeParameters.params; + + if ( + j.TSTypeReference.check(refType) && + isLiteralOrReference(j, propType) + ) { + renderFunction.params[0].typeAnnotation = + buildPropsAndRefIntersectionTypeAnnotation(j, propType, refType); + + isDirty = true; + } + } + + return renderFunction; + }); + + /** + * handle import + */ + if (isDirty) { + root + .find(j.ImportDeclaration, { + source: { + value: 'react', + }, + }) + .forEach((importDeclarationPath) => { + const { specifiers, importKind } = importDeclarationPath.node; + + if (importKind !== 'value') { + return; + } + + const specifiersWithoutForwardRef = + specifiers?.filter( + (s) => + !j.ImportSpecifier.check(s) || s.imported.name !== 'forwardRef', + ) ?? []; + + if (specifiersWithoutForwardRef.length === 0) { + j(importDeclarationPath).remove(); + } + + importDeclarationPath.node.specifiers = specifiersWithoutForwardRef; + }); + } + + return isDirty ? root.toSource() : undefined; +} diff --git a/codemods/legacy/transforms/rename-unsafe-lifecycles.js b/codemods/legacy/transforms/rename-unsafe-lifecycles.js new file mode 100644 index 0000000..09bc78b --- /dev/null +++ b/codemods/legacy/transforms/rename-unsafe-lifecycles.js @@ -0,0 +1,62 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +const DEPRECATED_APIS = Object.create(null); +DEPRECATED_APIS.componentWillMount = 'UNSAFE_componentWillMount'; +DEPRECATED_APIS.componentWillReceiveProps = 'UNSAFE_componentWillReceiveProps'; +DEPRECATED_APIS.componentWillUpdate = 'UNSAFE_componentWillUpdate'; + +export default (file, api, options) => { + const j = api.jscodeshift; + + const printOptions = options.printOptions || { + quote: 'single', + trailingComma: true + }; + + const root = j(file.source); + + let hasModifications = false; + + const renameDeprecatedApis = path => { + const name = path.node.key.name; + + if (DEPRECATED_APIS[name]) { + path.value.key.name = DEPRECATED_APIS[name]; + hasModifications = true; + } + }; + + const renameDeprecatedCallExpressions = path => { + const name = path.node.property.name; + + if (DEPRECATED_APIS[name]) { + path.node.property.name = DEPRECATED_APIS[name]; + hasModifications = true; + } + }; + + // Class methods + root.find(j.MethodDefinition).forEach(renameDeprecatedApis); + + // Class methods - typescript + root.find(j.ClassMethod).forEach(renameDeprecatedApis); + + // Arrow functions + root.find(j.ClassProperty).forEach(renameDeprecatedApis); + + // createReactClass and mixins + root.find(j.Property).forEach(renameDeprecatedApis); + + // Function calls + root.find(j.MemberExpression).forEach(renameDeprecatedCallExpressions); + + return hasModifications ? root.toSource(printOptions) : null; +}; diff --git a/codemods/legacy/transforms/sort-comp.js b/codemods/legacy/transforms/sort-comp.js new file mode 100644 index 0000000..6e8adf6 --- /dev/null +++ b/codemods/legacy/transforms/sort-comp.js @@ -0,0 +1,239 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +/** + * Reorders React component methods to match the [ESLint](http://eslint.org/) + * [react/sort-comp rule](https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/sort-comp.md), + * specifically with the [tighter constraints of the Airbnb style guide] + * (https://github.com/airbnb/javascript/blob/7684892951ef663e1c4e62ad57d662e9b2748b9e/\ + * packages/eslint-config-airbnb/rules/react.js#L122-L134), + * + * 'react/sort-comp': [2, { + * 'order': [ + * 'static-methods', + * 'lifecycle', + * '/^on.+$/', + * '/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/', + * 'everything-else', + * '/^render.+$/', + * 'render' + * ] + * }], + */ + +module.exports = function(fileInfo, api, options) { + const j = api.jscodeshift; + + const ReactUtils = require('./utils/ReactUtils')(j); + + const printOptions = options.printOptions || { + quote: 'single', + trailingComma: true + }; + + const methodsOrder = getMethodsOrder(fileInfo, options); // eslint-disable-line no-use-before-define + + const root = j(fileInfo.source); + + const propertyComparator = (a, b) => { + const nameA = a.key.name; + const nameB = b.key.name; + + const indexA = getCorrectIndex(methodsOrder, a); // eslint-disable-line no-use-before-define + const indexB = getCorrectIndex(methodsOrder, b); // eslint-disable-line no-use-before-define + + const sameLocation = indexA === indexB; + + if (sameLocation) { + // compare lexically + return +(nameA > nameB) || +(nameA === nameB) - 1; + } else { + // compare by index + return indexA - indexB; + } + }; + + const sortComponentProperties = classPath => { + const spec = ReactUtils.getReactCreateClassSpec(classPath); + + if (spec) { + spec.properties.sort(propertyComparator); + } + }; + + const sortClassProperties = classPath => { + const spec = ReactUtils.getClassExtendReactSpec(classPath); + + if (spec) { + spec.body.sort(propertyComparator); + } + }; + + if (options['explicit-require'] === false || ReactUtils.hasReact(root)) { + const createClassSortCandidates = ReactUtils.findReactCreateClass(root); + const es6ClassSortCandidates = ReactUtils.findReactES6ClassDeclaration( + root + ); + + if (createClassSortCandidates.size() > 0) { + createClassSortCandidates.forEach(sortComponentProperties); + } + + if (es6ClassSortCandidates.size() > 0) { + es6ClassSortCandidates.forEach(sortClassProperties); + } + + if ( + createClassSortCandidates.size() > 0 || + es6ClassSortCandidates.size() > 0 + ) { + return root.toSource(printOptions); + } + } + + return null; +}; + +// Hard-coded for Airbnb style +const defaultMethodsOrder = [ + 'static-methods', + 'displayName', + 'propTypes', + 'contextTypes', + 'childContextTypes', + 'mixins', + 'statics', + 'defaultProps', + 'constructor', + 'getDefaultProps', + 'state', + 'getInitialState', + 'getChildContext', + 'getDerivedStateFromProps', + 'componentWillMount', + 'UNSAFE_componentWillMount', + 'componentDidMount', + 'componentWillReceiveProps', + 'UNSAFE_componentWillReceiveProps', + 'shouldComponentUpdate', + 'componentWillUpdate', + 'UNSAFE_componentWillUpdate', + 'getSnapshotBeforeUpdate', + 'componentDidUpdate', + 'componentDidCatch', + 'componentWillUnmount', + '/^on.+$/', + '/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/', + 'everything-else', + '/^render.+$/', + 'render' +]; + +// FROM https://github.com/yannickcr/eslint-plugin-react/blob/master/lib/rules/sort-comp.js +const regExpRegExp = /\/(.*)\/([g|y|i|m]*)/; + +function selectorMatches(selector, method) { + const methodName = method.key.name; + + if ( + method.static && + selector === 'static-methods' && + defaultMethodsOrder.indexOf(methodName) === -1 + ) { + return true; + } + + if ( + !method.value && + method.typeAnnotation && + selector === 'type-annotations' + ) { + return true; + } + + if (method.static && selector === 'static-methods') { + return true; + } + + if (selector === methodName) { + return true; + } + + const selectorIsRe = regExpRegExp.test(selector); + + if (selectorIsRe) { + const match = selector.match(regExpRegExp); + const selectorRe = new RegExp(match[1], match[2]); + return selectorRe.test(methodName); + } + + return false; +} + +/** + * Get index of the matching patterns in methods order configuration + * @param {Object} method + * @returns {Number} Index of the method in the method ordering. Return [Infinity] if there is no match. + */ +function getCorrectIndex(methodsOrder, method) { + const everythingElseIndex = methodsOrder.indexOf('everything-else'); + + for (let i = 0; i < methodsOrder.length; i++) { + if (i != everythingElseIndex && selectorMatches(methodsOrder[i], method)) { + return i; + } + } + + if (everythingElseIndex >= 0) { + return everythingElseIndex; + } else { + return Infinity; + } +} + +function getMethodsOrderFromEslint(filePath) { + const CLIEngine = require('eslint').CLIEngine; + const cli = new CLIEngine({ useEslintrc: true }); + try { + const config = cli.getConfigForFile(filePath); + const { rules } = config; + const sortCompRules = rules['react/sort-comp']; + const ruleConfig = sortCompRules && sortCompRules[1]; + if (!ruleConfig) { + return null; + } + + const order = ruleConfig.order; + const groups = ruleConfig.groups || {}; + + let resolvedOrder = []; + for (let i = 0; i < order.length; i++) { + const entry = order[i]; + if (groups[entry]) { + resolvedOrder = resolvedOrder.concat(groups[entry]); + } else { + resolvedOrder.push(entry); + } + } + + return resolvedOrder; + } catch (e) { + // unable to get config for file + } + return null; +} + +function getMethodsOrder(fileInfo, options) { + return ( + options.methodsOrder || + getMethodsOrderFromEslint(fileInfo.path) || + defaultMethodsOrder + ); +} diff --git a/codemods/legacy/transforms/update-react-imports.js b/codemods/legacy/transforms/update-react-imports.js new file mode 100644 index 0000000..2871541 --- /dev/null +++ b/codemods/legacy/transforms/update-react-imports.js @@ -0,0 +1,380 @@ +/** + * (c) Facebook, Inc. and its affiliates. Confidential and proprietary. + * + * @format + */ + +'use strict'; + +module.exports = function(file, api, options) { + const j = api.jscodeshift; + const printOptions = options.printOptions || {}; + const root = j(file.source); + const destructureNamespaceImports = options.destructureNamespaceImports; + + // https://github.com/facebook/jscodeshift/blob/master/recipes/retain-first-comment.md + function getFirstNode() { + return root.find(j.Program).get('body', 0).node; + } + + // Save the comments attached to the first node + const firstNode = getFirstNode(); + const { comments } = firstNode; + + function isVariableDeclared(variable) { + return ( + root + .find(j.Identifier, { + name: variable, + }) + .filter( + path => + path.parent.value.type !== 'MemberExpression' && + path.parent.value.type !== 'QualifiedTypeIdentifier' && + path.parent.value.type !== 'JSXMemberExpression', + ) + .size() > 0 + ); + } + + // Get all paths that import from React + const reactImportPaths = root + .find(j.ImportDeclaration, { + type: 'ImportDeclaration', + }) + .filter(path => { + return ( + ( + path.value.source.type === 'Literal' || + path.value.source.type === 'StringLiteral' + ) && ( + path.value.source.value === 'React' || + path.value.source.value === 'react' + ) + ); + }); + + // get all namespace/default React imports + const reactPaths = reactImportPaths.filter(path => { + return ( + path.value.specifiers.length > 0 && + path.value.importKind === 'value' && + path.value.specifiers.some(specifier => specifier.local.name === 'React') + ); + }); + + if (reactPaths.size() > 1) { + throw Error( + 'There should only be one React import. Please remove the duplicate import and try again.', + ); + } + + if (reactPaths.size() === 0) { + return null; + } + + const reactPath = reactPaths.paths()[0]; + // Reuse the node so that we can preserve quoting style. + const reactLiteral = reactPath.value.source; + + const isDefaultImport = reactPath.value.specifiers.some( + specifier => + specifier.type === 'ImportDefaultSpecifier' && + specifier.local.name === 'React', + ); + + // Check to see if we should keep the React import + const isReactImportUsed = + root + .find(j.Identifier, { + name: 'React', + }) + .filter(path => { + return path.parent.parent.value.type !== 'ImportDeclaration'; + }) + .size() > 0; + + // local: imported + const reactIdentifiers = {}; + const reactTypeIdentifiers = {}; + let canDestructureReactVariable = false; + if (isReactImportUsed && (isDefaultImport || destructureNamespaceImports)) { + // Checks to see if the react variable is used itself (rather than used to access its properties) + canDestructureReactVariable = + root + .find(j.Identifier, { + name: 'React', + }) + .filter(path => { + return path.parent.parent.value.type !== 'ImportDeclaration'; + }) + .filter( + path => + !( + path.parent.value.type === 'MemberExpression' && + path.parent.value.object.name === 'React' + ) && + !( + path.parent.value.type === 'QualifiedTypeIdentifier' && + path.parent.value.qualification.name === 'React' + ) && + !( + path.parent.value.type === 'JSXMemberExpression' && + path.parent.value.object.name === 'React' + ), + ) + .size() === 0; + + if (canDestructureReactVariable) { + // Add React identifiers to separate object so we can destructure the imports + // later if we can. If a type variable that we are trying to import has already + // been declared, do not try to destructure imports + // (ex. Element is declared and we are using React.Element) + root + .find(j.QualifiedTypeIdentifier, { + qualification: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.id.name; + if (path.parent.parent.value.type === 'TypeofTypeAnnotation') { + // This is a typeof import so it isn't actually a type + reactIdentifiers[id] = id; + + if (reactTypeIdentifiers[id]) { + canDestructureReactVariable = false; + } + } else { + reactTypeIdentifiers[id] = id; + + if (reactIdentifiers[id]) { + canDestructureReactVariable = false; + } + } + + if (isVariableDeclared(id)) { + canDestructureReactVariable = false; + } + }); + + // Add React identifiers to separate object so we can destructure the imports + // later if we can. If a variable that we are trying to import has already + // been declared, do not try to destructure imports + // (ex. createElement is declared and we are using React.createElement) + root + .find(j.MemberExpression, { + object: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + reactIdentifiers[property] = property; + + if (isVariableDeclared(property) || reactTypeIdentifiers[property]) { + canDestructureReactVariable = false; + } + }); + + // Add React identifiers to separate object so we can destructure the imports + // later if we can. If a JSX variable that we are trying to import has already + // been declared, do not try to destructure imports + // (ex. Fragment is declared and we are using React.Fragment) + root + .find(j.JSXMemberExpression, { + object: { + type: 'JSXIdentifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + reactIdentifiers[property] = property; + + if (isVariableDeclared(property) || reactTypeIdentifiers[property]) { + canDestructureReactVariable = false; + } + }); + } + } + + if (canDestructureReactVariable) { + // replace react identifiers + root + .find(j.QualifiedTypeIdentifier, { + qualification: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const id = path.value.id.name; + + j(path).replaceWith(j.identifier(id)); + }); + + root + .find(j.MemberExpression, { + object: { + type: 'Identifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + + j(path).replaceWith(j.identifier(property)); + }); + + root + .find(j.JSXMemberExpression, { + object: { + type: 'JSXIdentifier', + name: 'React', + }, + }) + .forEach(path => { + const property = path.value.property.name; + + j(path).replaceWith(j.jsxIdentifier(property)); + }); + + // Add exisiting React imports to map + reactImportPaths.forEach(path => { + const specifiers = path.value.specifiers; + for (let i = 0; i < specifiers.length; i++) { + const specifier = specifiers[i]; + // get all type and regular imports that are imported + // from React + if (specifier.type === 'ImportSpecifier') { + if ( + path.value.importKind === 'type' || + specifier.importKind === 'type' + ) { + reactTypeIdentifiers[specifier.local.name] = + specifier.imported.name; + } else { + reactIdentifiers[specifier.local.name] = specifier.imported.name; + } + } + } + }); + + const regularImports = []; + Object.keys(reactIdentifiers).forEach(local => { + const imported = reactIdentifiers[local]; + regularImports.push( + j.importSpecifier(j.identifier(imported), j.identifier(local)), + ); + }); + + const typeImports = []; + Object.keys(reactTypeIdentifiers).forEach(local => { + const imported = reactTypeIdentifiers[local]; + typeImports.push( + j.importSpecifier(j.identifier(imported), j.identifier(local)), + ); + }); + + if (regularImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(regularImports, reactLiteral), + ); + } + if (typeImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(typeImports, reactLiteral, 'type'), + ); + } + + // remove all old react imports + reactImportPaths.forEach(path => { + // This is for import type React from 'react' which shouldn't + // be removed + if ( + path.value.specifiers.some( + specifier => + specifier.type === 'ImportDefaultSpecifier' && + specifier.local.name === 'React' && + (specifier.importKind === 'type' || + path.value.importKind === 'type'), + ) + ) { + j(path).insertAfter( + j.importDeclaration( + [j.importDefaultSpecifier(j.identifier('React'))], + reactLiteral, + 'type', + ), + ); + } + j(path).remove(); + }); + } else { + // Remove the import because it's not being used + // If we should keep the React import, just convert + // default imports to named imports + let isImportRemoved = false; + const specifiers = reactPath.value.specifiers; + for (let i = 0; i < specifiers.length; i++) { + const specifier = specifiers[i]; + if (specifier.type === 'ImportNamespaceSpecifier') { + if (!isReactImportUsed) { + isImportRemoved = true; + j(reactPath).remove(); + } + } else if (specifier.type === 'ImportDefaultSpecifier') { + if (isReactImportUsed) { + j(reactPath).insertAfter( + j.importDeclaration( + [j.importNamespaceSpecifier(j.identifier('React'))], + reactLiteral, + ), + ); + } + + if (specifiers.length > 1) { + const typeImports = []; + const regularImports = []; + for (let x = 0; x < specifiers.length; x++) { + if (specifiers[x].type !== 'ImportDefaultSpecifier') { + if (specifiers[x].importKind === 'type') { + typeImports.push(specifiers[x]); + } else { + regularImports.push(specifiers[x]); + } + } + } + if (regularImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(regularImports, reactLiteral), + ); + } + if (typeImports.length > 0) { + j(reactPath).insertAfter( + j.importDeclaration(typeImports, reactLiteral, 'type'), + ); + } + } + + isImportRemoved = true; + j(reactPath).remove(); + } + } + + if (!isImportRemoved) { + return null; + } + } + + // If the first node has been modified or deleted, reattach the comments + const firstNode2 = getFirstNode(); + if (firstNode2 !== firstNode) { + firstNode2.comments = comments; + } + + return root.toSource(printOptions); +}; diff --git a/codemods/legacy/transforms/utils/ReactUtils.js b/codemods/legacy/transforms/utils/ReactUtils.js new file mode 100644 index 0000000..2a6a3b1 --- /dev/null +++ b/codemods/legacy/transforms/utils/ReactUtils.js @@ -0,0 +1,308 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +module.exports = function(j) { + const REACT_CREATE_CLASS_MEMBER_EXPRESSION = { + type: 'MemberExpression', + object: { + name: 'React', + }, + property: { + name: 'createClass', + }, + }; + + // --------------------------------------------------------------------------- + // Checks if the file requires a certain module + const hasModule = (path, module) => + path + .findVariableDeclarators() + .filter(j.filters.VariableDeclarator.requiresModule(module)) + .size() === 1 || + path + .find(j.ImportDeclaration, { + type: 'ImportDeclaration', + source: { + type: 'Literal', + }, + }) + .filter(declarator => declarator.value.source.value === module) + .size() === 1; + + const hasReact = path => ( + hasModule(path, 'React') || + hasModule(path, 'react') || + hasModule(path, 'react/addons') || + hasModule(path, 'react-native') + ); + + // --------------------------------------------------------------------------- + // Finds all variable declarations that call React.createClass + const findReactCreateClassCallExpression = path => + j(path).find(j.CallExpression, { + callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, + }); + + const findReactCreateClass = path => + path + .findVariableDeclarators() + .filter(decl => findReactCreateClassCallExpression(decl).size() > 0); + + const findReactCreateClassExportDefault = path => + path.find(j.ExportDeclaration, { + default: true, + declaration: { + type: 'CallExpression', + callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, + }, + }); + + const findReactCreateClassModuleExports = path => + path + .find(j.AssignmentExpression, { + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'module', + }, + property: { + type: 'Identifier', + name: 'exports', + }, + }, + right: { + type: 'CallExpression', + callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, + }, + }); + + const getReactCreateClassSpec = classPath => { + const {value} = classPath; + const args = (value.init || value.right || value.declaration).arguments; + if (args && args.length) { + const spec = args[0]; + if (spec.type === 'ObjectExpression' && Array.isArray(spec.properties)) { + return spec; + } + } + return null; + }; + + // --------------------------------------------------------------------------- + // Finds alias for React.Component if used as named import. + const findReactComponentNameByParent = (path, parentClassName) => { + const reactImportDeclaration = path + .find(j.ImportDeclaration, { + type: 'ImportDeclaration', + source: { + type: 'Literal', + }, + }) + .filter(importDeclaration => hasReact(path)); + + const componentImportSpecifier = reactImportDeclaration + .find(j.ImportSpecifier, { + type: 'ImportSpecifier', + imported: { + type: 'Identifier', + name: parentClassName, + }, + }).at(0); + + const paths = componentImportSpecifier.paths(); + return paths.length + ? paths[0].value.local.name + : undefined; + }; + + const removeUnusedSuperClassImport = (path, file, superClassName) => { + if (path.find(j.Identifier, { + type: 'Identifier', + name: superClassName + }).length === 0) { + file.find(j.ImportSpecifier, { + type: 'ImportSpecifier', + imported: { + type: 'Identifier', + name: superClassName, + } + }).remove(); + } + }; + + const findReactES6ClassDeclarationByParent = (path, parentClassName) => { + const componentImport = findReactComponentNameByParent(path, parentClassName); + + const selector = componentImport + ? { + superClass: { + type: 'Identifier', + name: componentImport, + }, + } + : { + superClass: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'React', + }, + property: { + type: 'Identifier', + name: parentClassName, + }, + }, + }; + + return path + .find(j.ClassDeclaration, selector); + }; + + // Finds all classes that extend React.Component + const findReactES6ClassDeclaration = path => { + let classDeclarations = findReactES6ClassDeclarationByParent(path, 'Component'); + if (classDeclarations.size() === 0) { + classDeclarations = findReactES6ClassDeclarationByParent(path, 'PureComponent'); + } + return classDeclarations; + }; + + // --------------------------------------------------------------------------- + // Checks if the React class has mixins + const isMixinProperty = property => { + const key = property.key; + const value = property.value; + return ( + key.name === 'mixins' && + value.type === 'ArrayExpression' && + Array.isArray(value.elements) && + value.elements.length + ); + }; + + const hasMixins = classPath => { + const spec = getReactCreateClassSpec(classPath); + return spec && spec.properties.some(isMixinProperty); + }; + + // --------------------------------------------------------------------------- + // Others + const getClassExtendReactSpec = classPath => classPath.value.body; + + const createCreateReactClassCallExpression = properties => + j.callExpression( + j.memberExpression( + j.identifier('React'), + j.identifier('createClass'), + false + ), + [j.objectExpression(properties)] + ); + + const getComponentName = + classPath => classPath.node.id && classPath.node.id.name; + + // --------------------------------------------------------------------------- + // Direct methods! (see explanation below) + const findAllReactCreateClassCalls = path => + path.find(j.CallExpression, { + callee: REACT_CREATE_CLASS_MEMBER_EXPRESSION, + }); + + // Mixin Stuff + const containSameElements = (ls1, ls2) => { + if (ls1.length !== ls2.length) { + return false; + } + + return ( + ls1.reduce((res, x) => res && ls2.indexOf(x) !== -1, true) && + ls2.reduce((res, x) => res && ls1.indexOf(x) !== -1, true) + ); + }; + + const keyNameIsMixins = property => property.key.name === 'mixins'; + + const isSpecificMixinsProperty = (property, mixinIdentifierNames) => { + const key = property.key; + const value = property.value; + + return ( + key.name === 'mixins' && + value.type === 'ArrayExpression' && + Array.isArray(value.elements) && + value.elements.every(elem => elem.type === 'Identifier') && + containSameElements(value.elements.map(elem => elem.name), mixinIdentifierNames) + ); + }; + + // These following methods assume that the argument is + // a `React.createClass` call expression. In other words, + // they should only be used with `findAllReactCreateClassCalls`. + const directlyGetCreateClassSpec = classPath => { + if (!classPath || !classPath.value) { + return null; + } + const args = classPath.value.arguments; + if (args && args.length) { + const spec = args[0]; + if (spec.type === 'ObjectExpression' && Array.isArray(spec.properties)) { + return spec; + } + } + return null; + }; + + const directlyGetComponentName = classPath => { + let result = ''; + if ( + classPath.parentPath.value && + classPath.parentPath.value.type === 'VariableDeclarator' + ) { + result = classPath.parentPath.value.id.name; + } + return result; + }; + + const directlyHasMixinsField = classPath => { + const spec = directlyGetCreateClassSpec(classPath); + return spec && spec.properties.some(keyNameIsMixins); + }; + + const directlyHasSpecificMixins = (classPath, mixinIdentifierNames) => { + const spec = directlyGetCreateClassSpec(classPath); + return spec && spec.properties.some(prop => isSpecificMixinsProperty(prop, mixinIdentifierNames)); + }; + + return { + createCreateReactClassCallExpression, + findReactES6ClassDeclaration, + findReactCreateClass, + findReactCreateClassCallExpression, + findReactCreateClassModuleExports, + findReactCreateClassExportDefault, + getComponentName, + getReactCreateClassSpec, + getClassExtendReactSpec, + hasMixins, + hasModule, + hasReact, + isMixinProperty, + removeUnusedSuperClassImport, + + // "direct" methods + findAllReactCreateClassCalls, + directlyGetComponentName, + directlyGetCreateClassSpec, + directlyHasMixinsField, + directlyHasSpecificMixins, + }; +}; diff --git a/codemods/legacy/transforms/utils/array-polyfills.js b/codemods/legacy/transforms/utils/array-polyfills.js new file mode 100644 index 0000000..e0cbfde --- /dev/null +++ b/codemods/legacy/transforms/utils/array-polyfills.js @@ -0,0 +1,46 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +/*eslint-disable no-extend-native*/ + +'use strict'; + +function findIndex(predicate, context) { + if (this == null) { + throw new TypeError( + 'Array.prototype.findIndex called on null or undefined' + ); + } + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + var list = Object(this); + /* eslint-disable no-bitwise */ + var length = list.length >>> 0; + /* eslint-enable no-bitwise */ + for (var i = 0; i < length; i++) { + if (predicate.call(context, list[i], i, list)) { + return i; + } + } + return -1; +} + +if (!Array.prototype.findIndex) { + Array.prototype.findIndex = findIndex; +} + +if (!Array.prototype.find) { + Array.prototype.find = function(predicate, context) { + if (this == null) { + throw new TypeError('Array.prototype.find called on null or undefined'); + } + var index = findIndex.call(this, predicate, context); + return index === -1 ? undefined : this[index]; + }; +} diff --git a/codemods/legacy/transforms/utils/doesNotUseArguments.js b/codemods/legacy/transforms/utils/doesNotUseArguments.js new file mode 100644 index 0000000..fb1c135 --- /dev/null +++ b/codemods/legacy/transforms/utils/doesNotUseArguments.js @@ -0,0 +1,40 @@ +/** + * Copyright 2015-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +'use strict'; + +module.exports = function(j) { + const ReactUtils = require('./ReactUtils')(j); + + const doesNotUseArguments = (path, filepath) => { + const hasArguments = + j(path) + .find(j.Identifier, { name: 'arguments' }) + .size() > 0; + + if (hasArguments) { + var warnStr = ''; + + if (filepath) { + warnStr += filepath + ': '; + } + + warnStr += '`' + ReactUtils.directlyGetComponentName(path) + '` ' + + 'was skipped because `arguments` was found in your functions. ' + + 'Arrow functions do not expose an `arguments` object; ' + + 'consider changing to use ES6 spread operator and re-run this script.'; + + console.warn(warnStr); + + return false; + } + return true; + }; + + return doesNotUseArguments; +}; diff --git a/codemods/legacy/tsconfig.json b/codemods/legacy/tsconfig.json new file mode 100644 index 0000000..da0445e --- /dev/null +++ b/codemods/legacy/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "moduleDetection": "force", + "target": "ES2015" + } +} diff --git a/codemods/manual-bind-to-arrow/README.md b/codemods/manual-bind-to-arrow/README.md deleted file mode 100644 index 9007c9f..0000000 --- a/codemods/manual-bind-to-arrow/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/manual-bind-to-arrow - -Convert constructor-time `.bind(this)` assignments into arrow class fields. - -## Usage - -```bash -codemod run @react/manual-bind-to-arrow -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/manual-bind-to-arrow/codemod.yaml b/codemods/manual-bind-to-arrow/codemod.yaml deleted file mode 100644 index f0f0689..0000000 --- a/codemods/manual-bind-to-arrow/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/manual-bind-to-arrow" -version: "0.1.0" -description: "Convert this.method = this.method.bind(this) to arrow class property" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/manual-bind-to-arrow/package.json b/codemods/manual-bind-to-arrow/package.json deleted file mode 100644 index c398f49..0000000 --- a/codemods/manual-bind-to-arrow/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@react/manual-bind-to-arrow", - "version": "0.1.0", - "description": "Convert this.method = this.method.bind(this) to arrow class property", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "typescript": "latest", - "@types/node": "latest" - } -} diff --git a/codemods/manual-bind-to-arrow/scripts/codemod.ts b/codemods/manual-bind-to-arrow/scripts/codemod.ts deleted file mode 100644 index 462b54d..0000000 --- a/codemods/manual-bind-to-arrow/scripts/codemod.ts +++ /dev/null @@ -1,188 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - - -function methodDefByNameRule(methodName: string) { - const escaped = methodName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return { - kind: "method_definition" as const, - has: { - field: "name", - any: [ - { kind: "property_identifier" as const, regex: `^${escaped}$` }, - { kind: "identifier" as const, regex: `^${escaped}$` }, - ], - }, - }; -} - -const USES_ARGUMENTS_RULE = { - has: { kind: "identifier" as const, regex: "^arguments$" }, -}; - -function getClassBody(node: SgNode): SgNode | null { - const classDecl = node.ancestors().find((a) => a.kind() === "class_declaration"); - if (!classDecl) return null; - return classDecl.find({ rule: { kind: "class_body" } }); -} - -function memberPropertyName(node: SgNode | null): string | null { - return node?.kind() === "member_expression" ? (node.field("property")?.text() ?? null) : null; -} - -function memberObject(node: SgNode | null): SgNode | null { - return node?.kind() === "member_expression" ? node.field("object") : null; -} - -function isThisLikeObject(node: SgNode | null): boolean { - if (!node) return false; - if (node.text() === "this" || node.text() === "self") return true; - return node.kind() === "parenthesized_expression" && node.find({ rule: { kind: "this" } }) !== null; -} - -function isSuperStatement(node: SgNode): boolean { - const call = node.find({ rule: { kind: "call_expression" } }); - return node.kind() === "expression_statement" && call?.field("function")?.text() === "super"; -} - -function statementForAssignment(assign: SgNode): SgNode | null { - return assign.ancestors().find((a) => a.kind() === "expression_statement") ?? null; -} - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const source = rootNode.text(); - const edits: Edit[] = []; - const metric = useMetricAtom("manual-bind-to-arrow-transforms"); - - const allAssignments = rootNode.findAll({ rule: { kind: "assignment_expression" } }); - const allBindAssignments = allAssignments.filter((assign) => { - const right = assign.field("right"); - if (!right || right.kind() !== "call_expression") return false; - const callee = right.field("function"); - if (!callee || callee.kind() !== "member_expression") return false; - if (callee.field("property")?.text() !== "bind") return false; - const left = assign.field("left"); - if (!left || left.kind() !== "member_expression") return false; - if (!isThisLikeObject(left.field("object"))) return false; - return true; - }); - const assignmentsInConstructors = allBindAssignments.filter((assign) => { - const ctor = assign.ancestors().find((a) => a.kind() === "method_definition" && (a.field("name") ?? a.field("key"))?.text() === "constructor"); - return !!ctor; - }); - - const assignments = assignmentsInConstructors.filter((assign) => { - const left = assign.field("left"); - const right = assign.field("right"); - if (!left || !right) return false; - const bindCallee = right.field("function"); - if (!bindCallee || bindCallee.kind() !== "member_expression") return false; - const bindObj = bindCallee.field("object"); - if (!bindObj || memberObject(bindObj)?.text() !== "this") return false; - const bindObjProp = memberPropertyName(bindObj); - const leftProp = memberPropertyName(left); - return !!leftProp && !!bindObjProp && leftProp === bindObjProp; - }); - - for (const assign of assignments) { - const left = assign.field("left"); - const leftProp = left?.field("property"); - if (!leftProp) continue; - const methodName = leftProp.text(); - - const classBody = getClassBody(assign); - if (!classBody) continue; - - const methods = classBody.findAll({ rule: methodDefByNameRule(methodName) }); - const methodDef = methods[0]; - if (!methodDef || methodDef.find({ rule: USES_ARGUMENTS_RULE })) continue; - - const params = methodDef.field("parameters"); - const body = methodDef.field("body"); - if (!body) continue; - const stmt = assign.ancestors().find((a) => a.kind() === "expression_statement"); - const constructorNode = assign.ancestors().find((a) => a.kind() === "method_definition" && (a.field("name") ?? a.field("key"))?.text() === "constructor"); - const constructorBody = constructorNode?.field("body"); - const bindAssignmentsInConstructor = constructorNode - ? assignments - .filter((a) => { - const c = a.ancestors().find((anc) => anc.kind() === "method_definition" && (anc.field("name") ?? anc.field("key"))?.text() === "constructor"); - return c?.range().start.index === constructorNode.range().start.index; - }) - .sort((a, b) => a.range().start.index - b.range().start.index) - : []; - const selfIdentifiers = constructorNode?.findAll({ rule: { kind: "identifier", regex: "^self$" } }) ?? []; - const selfDeclaration = constructorNode?.findAll({ rule: { kind: "lexical_declaration" } }).find((decl) => - decl.find({ rule: { kind: "variable_declarator", has: { field: "name", kind: "identifier", regex: "^self$" } } }) !== null - ) ?? null; - const canRemoveSelfDeclaration = - left?.field("object")?.text() === "self" && selfIdentifiers.length <= 2 && !!selfDeclaration; - const removableBindStatements = new Set( - bindAssignmentsInConstructor - .map(statementForAssignment) - .filter((node): node is SgNode => node !== null) - .map((node) => node.range().start.index), - ); - const willBeEmptyConstructor = - !!constructorNode && - !!constructorBody && - constructorBody.children() - .filter((c) => c.isNamed()) - .every((child) => - isSuperStatement(child) || - removableBindStatements.has(child.range().start.index) || - (canRemoveSelfDeclaration && selfDeclaration?.range().start.index === child.range().start.index) - ); - - if (willBeEmptyConstructor && constructorNode) { - const ctorEnd = constructorNode.range().end.index; - const afterCtor = source.slice(ctorEnd, ctorEnd + 10); - const ctorTrailing = afterCtor.match(/^\s*\n?/)?.[0] ?? "\n"; - edits.push({ - startPos: constructorNode.range().start.index, - endPos: ctorEnd + ctorTrailing.length, - insertedText: "", - }); - } else if (stmt) { - const stmtEnd = stmt.range().end.index; - const after = source.slice(stmtEnd, stmtEnd + 10); - const trailing = after.match(/^\s*\n?/)?.[0] ?? "\n"; - edits.push({ - startPos: stmt.range().start.index, - endPos: stmtEnd + trailing.length, - insertedText: "", - }); - - if (canRemoveSelfDeclaration && selfDeclaration) { - const selfEnd = selfDeclaration.range().end.index; - const afterSelf = source.slice(selfEnd, selfEnd + 10); - const selfTrailing = afterSelf.match(/^\s*\n?/)?.[0] ?? "\n"; - edits.push({ - startPos: selfDeclaration.range().start.index, - endPos: selfEnd + selfTrailing.length, - insertedText: "", - }); - } - } - - const paramsText = params ? params.text() : "()"; - const returnTypeText = methodDef.field("return_type")?.text() ?? ""; - const bodyText = body.text(); - const arrowReplacement = `${methodName} = ${paramsText}${returnTypeText} => ${bodyText};`; - edits.push(methodDef.replace(arrowReplacement)); - - metric.increment({ file: metricFile(root.filename()) }); - } - - if (edits.length === 0) return null; - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/metrics.json deleted file mode 100644 index 4de0031..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow1/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/manual-bind-to-arrow1/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/metrics.json deleted file mode 100644 index 6ce9633..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow10/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/manual-bind-to-arrow10/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/input.tsx deleted file mode 100644 index ddcc601..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow11/input.tsx +++ /dev/null @@ -1,12 +0,0 @@ -class Component extends React.Component { - constructor() { - super(); - this.onClick = this.onClick.bind(this); - } - - onClick() { - if (fn) { - fn.apply(this, Array.prototype.slice.call(arguments, 1)) - } - } -} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/metrics.json deleted file mode 100644 index a426ece..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow2/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/manual-bind-to-arrow2/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/input.tsx b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/input.tsx deleted file mode 100644 index 8687c1f..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow3/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -class Component extends React.Component { - constructor() { - super(); - this.onClick = this.onClick.bind(this); - } - notOnClick() { } -} diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/metrics.json deleted file mode 100644 index 574360f..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow4/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/manual-bind-to-arrow4/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/metrics.json deleted file mode 100644 index 3f87734..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow5/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/manual-bind-to-arrow5/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/metrics.json deleted file mode 100644 index 22e736c..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow6/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/manual-bind-to-arrow6/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/metrics.json deleted file mode 100644 index 475fc7f..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow7/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/manual-bind-to-arrow7/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/metrics.json deleted file mode 100644 index b283aae..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow8/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/manual-bind-to-arrow8/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/metrics.json b/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/metrics.json deleted file mode 100644 index a615d80..0000000 --- a/codemods/manual-bind-to-arrow/tests/manual-bind-to-arrow9/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "manual-bind-to-arrow-transforms": [ - { - "cardinality": { - "file": "tests/manual-bind-to-arrow9/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/manual-bind-to-arrow/tsconfig.json b/codemods/manual-bind-to-arrow/tsconfig.json deleted file mode 100644 index ecbb40a..0000000 --- a/codemods/manual-bind-to-arrow/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "strict": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/manual-bind-to-arrow/workflow.yaml b/codemods/manual-bind-to-arrow/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/manual-bind-to-arrow/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/pure-component/README.md b/codemods/pure-component/README.md deleted file mode 100644 index 1a2db23..0000000 --- a/codemods/pure-component/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/pure-component - -Convert simple React classes into pure function components. - -## Usage - -```bash -codemod run @react/pure-component -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/pure-component/codemod.yaml b/codemods/pure-component/codemod.yaml deleted file mode 100644 index b68e686..0000000 --- a/codemods/pure-component/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/pure-component" -version: "0.1.0" -description: "Convert simple React classes into pure function components" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React", "PureComponent"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/pure-component/package.json b/codemods/pure-component/package.json deleted file mode 100644 index c18d5a9..0000000 --- a/codemods/pure-component/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@react/pure-component", - "version": "0.1.0", - "description": "Convert simple React classes into pure function components", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts && node --test ./scripts/warning-tests.mjs ./scripts/differential-tests.mjs", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "@types/node": "latest", - "typescript": "latest" - } -} diff --git a/codemods/pure-component/scripts/codemod.ts b/codemods/pure-component/scripts/codemod.ts deleted file mode 100644 index 8a06b02..0000000 --- a/codemods/pure-component/scripts/codemod.ts +++ /dev/null @@ -1,583 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -type ReactImportAliases = { - component: Set; - pureComponent: Set; - removableNamed: Set; -}; - -type ReactSuperclassKind = "Component" | "PureComponent"; - -const REACT_SOURCES = new Set(["react", "React", "react/addons", "react-native"]); - -function truthyParam(value: unknown): boolean { - return value === true || value === "true"; -} - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -function namedChildren(node: SgNode | null): SgNode[] { - if (!node) return []; - return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function importSource(importStmt: SgNode): SgNode | null { - return importStmt.field("source") ?? importStmt.find({ rule: { kind: "string" } }); -} - -function isReactSource(importStmt: SgNode): boolean { - const source = importSource(importStmt); - const value = source ? sourceText(source) : null; - return value !== null && REACT_SOURCES.has(value); -} - -function reactImportAliases(rootNode: SgNode): ReactImportAliases { - const component = new Set(); - const pureComponent = new Set(); - const removableNamed = new Set(); - - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (!isReactSource(importStmt)) continue; - - for (const specifier of importStmt.findAll({ rule: { kind: "import_specifier" } })) { - const imported = specifier.field("name")?.text(); - const aliasNode = specifier.field("alias"); - const alias = aliasNode?.text() ?? imported; - if (!imported || !alias) continue; - - if (imported === "Component") { - component.add(alias); - } else if (imported === "PureComponent") { - pureComponent.add(alias); - } else { - continue; - } - - // Legacy removes an import specifier by searching for its `imported` name - // in the rest of the file. For `{ Component as C }`, imported=Component but - // the class extends `C`; the search finds no `Component` identifier and the - // specifier gets kept (legacy quirk). Mirror that: only flag as removable - // when the local name equals the imported name. - if (!aliasNode) removableNamed.add(alias); - } - } - - return { component, pureComponent, removableNamed }; -} - -function reactSuperclassKind( - classDecl: SgNode, - aliases: ReactImportAliases, -): ReactSuperclassKind | null { - const heritage = classDecl.find({ rule: { kind: "class_heritage" } }); - if (!heritage) return null; - - const directExtends = heritage.find({ - rule: { any: [{ kind: "member_expression" }, { kind: "identifier" }] }, - }); - if (!directExtends) return null; - - if (directExtends.kind() === "member_expression") { - const object = directExtends.field("object"); - const property = directExtends.field("property")?.text() ?? ""; - if (object?.text() !== "React") return null; - if (property === "Component" || property === "PureComponent") return property; - return null; - } - - const name = directExtends.text(); - if (aliases.component.has(name)) return "Component"; - if (aliases.pureComponent.has(name)) return "PureComponent"; - return null; -} - -function classBodyMembers(classDecl: SgNode): SgNode[] { - const body = classDecl.find({ rule: { kind: "class_body" } }); - return namedChildren(body).filter( - (m) => m.kind() === "method_definition" || m.kind() === "public_field_definition", - ); -} - -function memberName(member: SgNode): string { - return member.field("name")?.text() ?? ""; -} - -function isRenderMethod(member: SgNode): boolean { - return member.kind() === "method_definition" && memberName(member) === "render"; -} - -function isStaticField(member: SgNode): boolean { - if (member.kind() !== "public_field_definition") return false; - return member.children().some((child) => child.kind() === "static" || child.text() === "static"); -} - -function isPropsTypeField(member: SgNode): boolean { - return member.kind() === "public_field_definition" && - !isStaticField(member) && - memberName(member) === "props"; -} - -function onlyHasSafeMembers(classDecl: SgNode): boolean { - const members = classBodyMembers(classDecl); - const methods = members.filter((m) => m.kind() === "method_definition"); - if (methods.length !== 1 || !isRenderMethod(methods[0]!)) return false; - return members.every((m) => isRenderMethod(m) || isPropsTypeField(m) || isStaticField(m)); -} - -function hasRefAttribute(classDecl: SgNode): boolean { - return classDecl.findAll({ rule: { kind: "jsx_attribute" } }) - .some((attr) => namedChildren(attr)[0]?.text() === "ref"); -} - -function renderMethod(classDecl: SgNode): SgNode | null { - return classBodyMembers(classDecl).find(isRenderMethod) ?? null; -} - -function propsField(classDecl: SgNode): SgNode | null { - return classBodyMembers(classDecl).find(isPropsTypeField) ?? null; -} - -function propsTypeAnnotationText(classDecl: SgNode): string { - const annotation = propsField(classDecl)?.find({ rule: { kind: "type_annotation" } }); - return annotation?.text() ?? ""; -} - -function propsObjectType(classDecl: SgNode): SgNode | null { - const annotation = propsField(classDecl)?.find({ rule: { kind: "type_annotation" } }); - const typeNode = namedChildren(annotation)[0] ?? null; - return typeNode?.kind() === "object_type" ? typeNode : null; -} - -function isThisProps(node: SgNode | null | undefined): boolean { - if (!node || node.kind() !== "member_expression") return false; - const object = node.field("object"); - const property = node.field("property"); - return object?.kind() === "this" && property?.text() === "props"; -} - -function thisPropsExpressions(body: SgNode): SgNode[] { - return body.findAll({ rule: { kind: "member_expression" } }).filter(isThisProps); -} - -function propsAccesses(body: SgNode): SgNode[] { - return body.findAll({ rule: { kind: "member_expression" } }) - .filter((m) => isThisProps(m.field("object"))); -} - -function barePropsIdentifiers(methodDef: SgNode): boolean { - return methodDef.findAll({ - rule: { - any: [ - { kind: "identifier", regex: "^props$" }, - { kind: "property_identifier", regex: "^props$" }, - ], - }, - }).some((identifier) => { - const parent = identifier.parent(); - const grand = parent?.parent(); - if ( - parent?.kind() === "member_expression" && - grand?.kind() === "member_expression" && - grand.field("object")?.id() === parent.id() - ) { - return false; - } - return true; - }); -} - -function duplicateDeclarators(body: SgNode): Map> { - const duplicates = new Map>(); - for (const declarator of body.findAll({ rule: { kind: "variable_declarator" } })) { - const name = declarator.field("name"); - const value = declarator.field("value"); - if (name?.kind() !== "identifier" || value?.kind() !== "member_expression") continue; - const object = value.field("object"); - const property = value.field("property"); - if (!object || !property || !isThisProps(object) || property.text() !== name.text()) continue; - duplicates.set(name.text(), declarator); - } - return duplicates; -} - -function propNamesInOrder(body: SgNode): string[] { - const names: string[] = []; - const seen = new Set(); - for (const access of propsAccesses(body)) { - const property = access.field("property")?.text(); - if (property && !seen.has(property)) { - seen.add(property); - names.push(property); - } - } - return names; -} - -function hasAssignmentNamesShadowing( - body: SgNode, - propNames: string[], - duplicates: Map>, -): boolean { - const propNameSet = new Set(propNames); - const duplicateNames = new Set(duplicates.keys()); - - return body.findAll({ rule: { kind: "identifier" } }).some((identifier) => { - const name = identifier.text(); - if (!propNameSet.has(name) || duplicateNames.has(name)) return false; - const parent = identifier.parent(); - if (!parent) return true; - if (parent.kind() === "member_expression" && parent.field("property")?.id() === identifier.id()) { - return false; - } - const object = parent.kind() === "member_expression" ? parent.field("object") : null; - if (object && isThisProps(object)) return false; - return true; - }); -} - -function canDestructure( - classDecl: SgNode, - methodDef: SgNode, - body: SgNode, -): boolean { - const names = propNamesInOrder(body); - const duplicates = duplicateDeclarators(body); - if (barePropsIdentifiers(methodDef) || hasAssignmentNamesShadowing(body, names, duplicates)) { - return false; - } - // Without an inline object type we can't produce a typed destructure pattern. - // The legacy transform crashes in this case (calls `.properties.forEach` on a - // non-object type); we skip safely and fall back to `props: Annotation`. - const annotationText = propsTypeAnnotationText(classDecl); - if (annotationText === "") return true; - return propsObjectType(classDecl) !== null; -} - -function lineDeletionRange( - node: SgNode, - source: string, -): { start: number; end: number } { - const range = node.range(); - let start = range.start.index; - while (start > 0 && source[start - 1] !== "\n") start--; - let end = range.end.index; - if (end < source.length && source[end] === "\n") end++; - return { start, end }; -} - -function rangeContains( - outer: { start: number; end: number }, - inner: { start: number; end: number }, -): boolean { - return inner.start >= outer.start && inner.end <= outer.end; -} - -function rewriteRenderBody( - renderBody: SgNode, - source: string, - mayDestructure: boolean, - duplicates: Map>, -): string { - const edits: Edit[] = []; - const deletedRanges: Array<{ start: number; end: number }> = []; - - if (mayDestructure) { - for (const declarator of duplicates.values()) { - const stmt = declarator.parent(); - if (!stmt) continue; - if (stmt.kind() !== "lexical_declaration" && stmt.kind() !== "variable_declaration") continue; - const declarators = stmt.findAll({ rule: { kind: "variable_declarator" } }); - if (declarators.length !== 1) continue; - const range = lineDeletionRange(stmt, source); - deletedRanges.push(range); - edits.push({ startPos: range.start, endPos: range.end, insertedText: "" }); - } - } - - const inDeleted = (node: SgNode) => { - const r = node.range(); - const span = { start: r.start.index, end: r.end.index }; - return deletedRanges.some((dr) => rangeContains(dr, span)); - }; - - if (mayDestructure) { - for (const access of propsAccesses(renderBody)) { - if (inDeleted(access)) continue; - const prop = access.field("property")?.text(); - if (prop) edits.push(access.replace(prop)); - } - } else { - for (const me of thisPropsExpressions(renderBody)) { - edits.push(me.replace("props")); - } - } - - return renderBody.commitEdits(edits); -} - -function normalizeBlock(blockText: string, indent: string): string { - const lines = blockText.split("\n"); - if (lines.length <= 2) return blockText; - - const inner = lines.slice(1, -1); - const nonEmpty = inner.filter((line) => line.trim().length > 0); - const commonIndent = nonEmpty.length === 0 - ? 0 - : Math.min(...nonEmpty.map((line) => line.match(/^ */)?.[0].length ?? 0)); - const body = inner - .map((line) => (line.length >= commonIndent ? line.slice(commonIndent) : line)) - .map((line) => (line.trim().length > 0 ? `${indent}${line}` : "")) - .join("\n"); - return `{\n${body}\n}`; -} - -function renderDestructuredParam(names: string[], objectType: SgNode | null): string { - const typed = new Map(); - if (objectType) { - for (const prop of objectType.findAll({ rule: { kind: "property_signature" } })) { - const key = prop.field("name")?.text(); - const type = prop.find({ rule: { kind: "type_annotation" } })?.text().replace(/^:\s*/, ""); - if (key && type) typed.set(key, type); - } - } - - const lines = names.map((name) => - typed.has(name) ? ` ${name}: ${typed.get(name)},` : ` ${name},`, - ); - return `(\n {\n${lines.join("\n")}\n },\n)`; -} - -function renderFunctionLike( - name: string, - bodyText: string, - paramText: string, - useArrows: boolean, -): string { - const body = normalizeBlock(bodyText, " "); - - if (useArrows) { - let left: string; - if (paramText === "") left = "()"; - else if (paramText === "props") left = "props"; - else if (paramText.startsWith("(")) left = paramText; - else left = `(${paramText})`; - return `const ${name} = ${left} => ${body};`; - } - - if (paramText.startsWith("(")) { - return `function ${name}${paramText} ${body}`; - } - return `function ${name}(${paramText}) ${body}`; -} - -function renderStaticAssignments(className: string, classDecl: SgNode): string[] { - return classBodyMembers(classDecl) - .filter(isStaticField) - .map((field) => { - const name = field.field("name")?.text() ?? ""; - const value = field.field("value")?.text() ?? "undefined"; - return `${className}.${name} = ${value};`; - }); -} - -function getClassName(classDecl: SgNode): string | null { - return classDecl.field("name")?.text() ?? null; -} - -function skipWarning(classDecl: SgNode): string { - const name = getClassName(classDecl) ?? "Unknown"; - const filename = classDecl.getRoot().filename(); - const { line, column } = classDecl.range().start; - // Jscodeshift reports line as 1-indexed, column as 0-indexed. Ast-grep is - // 0-indexed on both; +1 on the line matches legacy output exactly. - return `Class "${name}" skipped in ${filename} on ${line + 1}:${column}`; -} - -function enclosingExportStatement(classDecl: SgNode): SgNode | null { - const parent = classDecl.parent(); - return parent?.kind() === "export_statement" ? parent : null; -} - -function isDefaultExportStatement(exportStmt: SgNode): boolean { - return exportStmt.children().some((c) => c.kind() === "default" || c.text() === "default"); -} - -function remainingNamedImportUse( - rootNode: SgNode, - alias: string, - transformedClassIds: Set, -): boolean { - return rootNode - .findAll({ rule: { kind: "identifier", regex: `^${alias}$` } }) - .some((identifier) => { - if (identifier.ancestors().some((a) => a.kind() === "import_statement")) return false; - const classDecl = identifier.ancestors().find((a) => a.kind() === "class_declaration"); - if (!classDecl) return true; - return !transformedClassIds.has(classDecl.id()); - }); -} - -function buildImportSpecifierEdits( - importStmt: SgNode, - aliasesToDrop: Set, - source: string, -): Edit[] { - const named = importStmt.find({ rule: { kind: "named_imports" } }); - if (!named) return []; - - const specifiers = named.findAll({ rule: { kind: "import_specifier" } }); - const toKeep = specifiers.filter((s) => { - const local = s.field("alias")?.text() ?? s.field("name")?.text() ?? ""; - return !aliasesToDrop.has(local); - }); - - if (toKeep.length === specifiers.length) return []; - - const clause = importStmt.find({ rule: { kind: "import_clause" } }); - const defaultImport = clause - ? namedChildren(clause).find((c) => c.kind() === "identifier") - : null; - - if (toKeep.length === 0) { - if (defaultImport) { - return [{ - startPos: clause!.range().start.index, - endPos: clause!.range().end.index, - insertedText: defaultImport.text(), - }]; - } - // Legacy uses ImportSpecifier.remove() which drops only the specifier and - // leaves `import 'source';` as a side-effect import. Match that by - // replacing the import_clause (plus the trailing `from`) with nothing. - const sourceNode = importSource(importStmt); - if (!clause || !sourceNode) { - const range = lineDeletionRange(importStmt, source); - return [{ startPos: range.start, endPos: range.end, insertedText: "" }]; - } - return [{ - startPos: clause.range().start.index, - endPos: sourceNode.range().start.index, - insertedText: "", - }]; - } - - const newSpecifiers = toKeep.map((s) => s.text()).join(", "); - return [{ - startPos: named.range().start.index, - endPos: named.range().end.index, - insertedText: `{ ${newSpecifiers} }`, - }]; -} - -const transform: Transform = async (root, options) => { - const rootNode = root.root(); - const source = rootNode.text(); - const metric = useMetricAtom("pure-component-conversions"); - const useArrows = truthyParam(options.params?.useArrows); - const destructuring = truthyParam(options.params?.destructuring); - const silenceWarnings = truthyParam(options.params?.silenceWarnings); - - const aliases = reactImportAliases(rootNode); - const edits: Edit[] = []; - const transformedClassIds = new Set(); - const reactClasses = rootNode.findAll({ rule: { kind: "class_declaration" } }) - .map((classDecl) => ({ classDecl, kind: reactSuperclassKind(classDecl, aliases) })) - .filter((entry): entry is { classDecl: SgNode; kind: ReactSuperclassKind } => entry.kind !== null); - const targetSuperclassKind = reactClasses.some((entry) => entry.kind === "Component") - ? "Component" - : reactClasses.some((entry) => entry.kind === "PureComponent") - ? "PureComponent" - : null; - - for (const { classDecl, kind } of reactClasses) { - if (targetSuperclassKind === null || kind !== targetSuperclassKind) continue; - - if (!onlyHasSafeMembers(classDecl) || hasRefAttribute(classDecl)) { - if (!silenceWarnings) console.warn(skipWarning(classDecl)); - continue; - } - - const name = getClassName(classDecl); - const render = renderMethod(classDecl); - const renderBody = render?.field("body"); - if (!name || !render || !renderBody) continue; - - const names = propNamesInOrder(renderBody); - const duplicates = duplicateDeclarators(renderBody); - const objectType = propsObjectType(classDecl); - const annotationText = propsTypeAnnotationText(classDecl); - const hasPropsUse = thisPropsExpressions(renderBody).length > 0; - const canDestr = canDestructure(classDecl, render, renderBody); - const mayDestructure = destructuring && hasPropsUse && canDestr; - - if (destructuring && !canDestr) { - console.warn(`Unable to destructure ${name} props.`); - } - - const newBodyText = rewriteRenderBody(renderBody, source, mayDestructure, duplicates); - - let paramText = ""; - if (hasPropsUse) { - if (mayDestructure) { - paramText = renderDestructuredParam(names, objectType); - } else { - paramText = annotationText ? `props${annotationText}` : "props"; - } - } - - let replacement = renderFunctionLike(name, newBodyText, paramText, useArrows); - const statics = renderStaticAssignments(name, classDecl); - if (statics.length > 0) replacement += `\n\n${statics.join("\n")}`; - - const exportStmt = enclosingExportStatement(classDecl); - const isDefault = exportStmt ? isDefaultExportStatement(exportStmt) : false; - const splitDefault = isDefault && useArrows; - - let editStart: number; - let editEnd: number; - if (splitDefault) { - editStart = exportStmt!.range().start.index; - editEnd = exportStmt!.range().end.index; - replacement = `${replacement}\nexport default ${name};`; - } else { - editStart = classDecl.range().start.index; - editEnd = classDecl.range().end.index; - } - - edits.push({ startPos: editStart, endPos: editEnd, insertedText: replacement }); - transformedClassIds.add(classDecl.id()); - } - - if (edits.length === 0) return null; - - const aliasesToDrop = new Set(); - for (const alias of aliases.removableNamed) { - if (!remainingNamedImportUse(rootNode, alias, transformedClassIds)) { - aliasesToDrop.add(alias); - } - } - - if (aliasesToDrop.size > 0) { - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (!isReactSource(importStmt)) continue; - edits.push(...buildImportSpecifierEdits(importStmt, aliasesToDrop, source)); - } - } - - metric.increment({ file: metricFile(root.filename()) }, transformedClassIds.size); - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/pure-component/scripts/differential-tests.mjs b/codemods/pure-component/scripts/differential-tests.mjs deleted file mode 100644 index c1ec390..0000000 --- a/codemods/pure-component/scripts/differential-tests.mjs +++ /dev/null @@ -1,80 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __filename = fileURLToPath(import.meta.url); -const packageDir = path.dirname(path.dirname(__filename)); -const codemodFile = path.join(packageDir, "scripts/codemod.ts"); -const testsDir = path.join(packageDir, "tests"); - -function readFixture(name, file) { - return readFileSync(path.join(testsDir, name, file), "utf8").trim(); -} - -function readParams(name) { - const configPath = path.join(testsDir, name, "test.config.json"); - try { - const config = JSON.parse(readFileSync(configPath, "utf8")); - return config.params ?? {}; - } catch { - return {}; - } -} - -function runJssgOutput(input, params = {}) { - const root = mkdtempSync(path.join(tmpdir(), "pure-component-jssg-")); - const caseDir = path.join(root, "case"); - mkdirSync(caseDir); - writeFileSync(path.join(caseDir, "input.tsx"), input, "utf8"); - writeFileSync(path.join(caseDir, "expected.tsx"), input, "utf8"); - if (Object.keys(params).length > 0) { - writeFileSync(path.join(caseDir, "test.config.json"), JSON.stringify({ params }, null, 2)); - } - - try { - const result = spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", - "jssg", - "test", - "-l", - "tsx", - codemodFile, - root, - "-u", - ], - { cwd: packageDir, encoding: "utf8", timeout: 20000 }, - ); - assert.strictEqual(result.status, 0, `${result.stdout}\n${result.stderr}`); - return readFileSync(path.join(caseDir, "expected.tsx"), "utf8").trim(); - } finally { - rmSync(root, { recursive: true, force: true }); - } -} - -const parityCases = [ - "named-import-component", - "named-import-purecomponent-alias", - "export-default-arrow", - "export-default-function", - "named-export", - "named-export-arrow", - "react-native-named-import", - "mixed-superclasses", -]; - -for (const name of parityCases) { - test(`matches checked-in parity fixture for ${name}`, () => { - const input = readFixture(name, "input.tsx"); - const expected = readFixture(name, "expected.tsx"); - const params = readParams(name); - const jssgOutput = runJssgOutput(input, params); - assert.strictEqual(jssgOutput, expected); - }); -} diff --git a/codemods/pure-component/scripts/warning-tests.mjs b/codemods/pure-component/scripts/warning-tests.mjs deleted file mode 100644 index 9940a0f..0000000 --- a/codemods/pure-component/scripts/warning-tests.mjs +++ /dev/null @@ -1,83 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __filename = fileURLToPath(import.meta.url); -const packageDir = path.dirname(path.dirname(__filename)); -const codemodFile = path.join(packageDir, "scripts/codemod.ts"); - -function runFixtureTest(input, params = {}) { - const root = mkdtempSync(path.join(tmpdir(), "pure-component-warn-")); - const caseDir = path.join(root, "case"); - mkdirSync(caseDir); - writeFileSync(path.join(caseDir, "input.tsx"), input, "utf8"); - writeFileSync(path.join(caseDir, "expected.tsx"), input, "utf8"); - if (Object.keys(params).length > 0) { - writeFileSync(path.join(caseDir, "test.config.json"), JSON.stringify({ params }, null, 2)); - } - - try { - return spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", - "jssg", - "test", - "-l", - "tsx", - codemodFile, - root, - "--strictness", - "loose", - ], - { - cwd: packageDir, - encoding: "utf8", - timeout: 20000, - }, - ); - } finally { - rmSync(root, { recursive: true, force: true }); - } -} - -test("warns when an impure class is skipped", () => { - const result = runFixtureTest(`import React from 'React'; -class Impure extends React.Component { - componentWillMount() {} - render() { return
    ; } -} -`); - const combined = `${result.stdout}\n${result.stderr}`; - assert.match(combined, /Class "Impure" skipped in/); -}); - -test("suppresses skip warnings with silenceWarnings", () => { - const result = runFixtureTest(`import React from 'React'; -class Impure extends React.Component { - componentWillMount() {} - render() { return
    ; } -} -`, { silenceWarnings: true }); - const combined = `${result.stdout}\n${result.stderr}`; - assert.doesNotMatch(combined, /Class "Impure" skipped in/); -}); - -test("warns when destructuring cannot be applied", () => { - const result = runFixtureTest(`import React from 'React'; -function doSomething(props) { return props; } -class UsesThisDotProps extends React.Component { - render() { - doSomething(this.props); - return
    ; - } -} -`, { destructuring: true }); - const combined = `${result.stdout}\n${result.stderr}`; - assert.match(combined, /Unable to destructure UsesThisDotProps props\./); -}); diff --git a/codemods/pure-component/tests/arrow/metrics.json b/codemods/pure-component/tests/arrow/metrics.json deleted file mode 100644 index 66d8789..0000000 --- a/codemods/pure-component/tests/arrow/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/arrow/input.tsx" - }, - "count": 5 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/arrow/test.config.json b/codemods/pure-component/tests/arrow/test.config.json deleted file mode 100644 index 8646dd5..0000000 --- a/codemods/pure-component/tests/arrow/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "useArrows": true - } -} diff --git a/codemods/pure-component/tests/default/metrics.json b/codemods/pure-component/tests/default/metrics.json deleted file mode 100644 index 71a5466..0000000 --- a/codemods/pure-component/tests/default/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/default/input.tsx" - }, - "count": 6 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/destructuring/metrics.json b/codemods/pure-component/tests/destructuring/metrics.json deleted file mode 100644 index 4f66453..0000000 --- a/codemods/pure-component/tests/destructuring/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/destructuring/input.tsx" - }, - "count": 6 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/destructuring/test.config.json b/codemods/pure-component/tests/destructuring/test.config.json deleted file mode 100644 index dffb75a..0000000 --- a/codemods/pure-component/tests/destructuring/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "destructuring": true - } -} diff --git a/codemods/pure-component/tests/export-default-arrow/expected.tsx b/codemods/pure-component/tests/export-default-arrow/expected.tsx deleted file mode 100644 index d8fe639..0000000 --- a/codemods/pure-component/tests/export-default-arrow/expected.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'React'; - -const Pure = () => { return
    ; }; -export default Pure; diff --git a/codemods/pure-component/tests/export-default-arrow/input.tsx b/codemods/pure-component/tests/export-default-arrow/input.tsx deleted file mode 100644 index 0f4bd1f..0000000 --- a/codemods/pure-component/tests/export-default-arrow/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'React'; - -export default class Pure extends React.Component { - render() { return
    ; } -} diff --git a/codemods/pure-component/tests/export-default-arrow/metrics.json b/codemods/pure-component/tests/export-default-arrow/metrics.json deleted file mode 100644 index 93360c5..0000000 --- a/codemods/pure-component/tests/export-default-arrow/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/export-default-arrow/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/export-default-arrow/test.config.json b/codemods/pure-component/tests/export-default-arrow/test.config.json deleted file mode 100644 index 8646dd5..0000000 --- a/codemods/pure-component/tests/export-default-arrow/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "useArrows": true - } -} diff --git a/codemods/pure-component/tests/export-default-function/expected.tsx b/codemods/pure-component/tests/export-default-function/expected.tsx deleted file mode 100644 index b87749b..0000000 --- a/codemods/pure-component/tests/export-default-function/expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'React'; - -export default function Pure() { return
    ; } diff --git a/codemods/pure-component/tests/export-default-function/input.tsx b/codemods/pure-component/tests/export-default-function/input.tsx deleted file mode 100644 index 0f4bd1f..0000000 --- a/codemods/pure-component/tests/export-default-function/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'React'; - -export default class Pure extends React.Component { - render() { return
    ; } -} diff --git a/codemods/pure-component/tests/export-default-function/metrics.json b/codemods/pure-component/tests/export-default-function/metrics.json deleted file mode 100644 index 5bcf0ec..0000000 --- a/codemods/pure-component/tests/export-default-function/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/export-default-function/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/mixed-superclasses/expected.tsx b/codemods/pure-component/tests/mixed-superclasses/expected.tsx deleted file mode 100644 index f788443..0000000 --- a/codemods/pure-component/tests/mixed-superclasses/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React, { PureComponent } from 'react'; - -function A(props) { return
    ; } - -class B extends PureComponent { - render() { return
    ; } -} diff --git a/codemods/pure-component/tests/mixed-superclasses/input.tsx b/codemods/pure-component/tests/mixed-superclasses/input.tsx deleted file mode 100644 index dcf2b03..0000000 --- a/codemods/pure-component/tests/mixed-superclasses/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React, { Component, PureComponent } from 'react'; - -class A extends Component { - render() { return
    ; } -} - -class B extends PureComponent { - render() { return
    ; } -} diff --git a/codemods/pure-component/tests/mixed-superclasses/metrics.json b/codemods/pure-component/tests/mixed-superclasses/metrics.json deleted file mode 100644 index b5fbb9c..0000000 --- a/codemods/pure-component/tests/mixed-superclasses/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/mixed-superclasses/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/named-export-arrow/expected.tsx b/codemods/pure-component/tests/named-export-arrow/expected.tsx deleted file mode 100644 index 9254115..0000000 --- a/codemods/pure-component/tests/named-export-arrow/expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -export const Pure = props => { return
    ; }; diff --git a/codemods/pure-component/tests/named-export-arrow/input.tsx b/codemods/pure-component/tests/named-export-arrow/input.tsx deleted file mode 100644 index 8708ae0..0000000 --- a/codemods/pure-component/tests/named-export-arrow/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { Component } from 'react'; - -export class Pure extends Component { - render() { return
    ; } -} diff --git a/codemods/pure-component/tests/named-export-arrow/metrics.json b/codemods/pure-component/tests/named-export-arrow/metrics.json deleted file mode 100644 index 2a89fa5..0000000 --- a/codemods/pure-component/tests/named-export-arrow/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/named-export-arrow/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/named-export-arrow/test.config.json b/codemods/pure-component/tests/named-export-arrow/test.config.json deleted file mode 100644 index 8646dd5..0000000 --- a/codemods/pure-component/tests/named-export-arrow/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "useArrows": true - } -} diff --git a/codemods/pure-component/tests/named-export/expected.tsx b/codemods/pure-component/tests/named-export/expected.tsx deleted file mode 100644 index e234c87..0000000 --- a/codemods/pure-component/tests/named-export/expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -export function Pure(props) { return
    ; } diff --git a/codemods/pure-component/tests/named-export/input.tsx b/codemods/pure-component/tests/named-export/input.tsx deleted file mode 100644 index 8708ae0..0000000 --- a/codemods/pure-component/tests/named-export/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { Component } from 'react'; - -export class Pure extends Component { - render() { return
    ; } -} diff --git a/codemods/pure-component/tests/named-export/metrics.json b/codemods/pure-component/tests/named-export/metrics.json deleted file mode 100644 index 6ca6bf0..0000000 --- a/codemods/pure-component/tests/named-export/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/named-export/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/named-import-component/expected.tsx b/codemods/pure-component/tests/named-import-component/expected.tsx deleted file mode 100644 index 904948f..0000000 --- a/codemods/pure-component/tests/named-import-component/expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import React from 'react'; - -function Pure(props) { return
    ; } diff --git a/codemods/pure-component/tests/named-import-component/input.tsx b/codemods/pure-component/tests/named-import-component/input.tsx deleted file mode 100644 index 2bd8659..0000000 --- a/codemods/pure-component/tests/named-import-component/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { Component } from 'react'; - -class Pure extends Component { - render() { return
    ; } -} diff --git a/codemods/pure-component/tests/named-import-component/metrics.json b/codemods/pure-component/tests/named-import-component/metrics.json deleted file mode 100644 index d4d500b..0000000 --- a/codemods/pure-component/tests/named-import-component/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/named-import-component/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/named-import-purecomponent-alias/expected.tsx b/codemods/pure-component/tests/named-import-purecomponent-alias/expected.tsx deleted file mode 100644 index 0cb2bd5..0000000 --- a/codemods/pure-component/tests/named-import-purecomponent-alias/expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import React, { PureComponent as P } from 'react'; - -function Pure(props) { return
    ; } diff --git a/codemods/pure-component/tests/named-import-purecomponent-alias/input.tsx b/codemods/pure-component/tests/named-import-purecomponent-alias/input.tsx deleted file mode 100644 index 270c814..0000000 --- a/codemods/pure-component/tests/named-import-purecomponent-alias/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React, { PureComponent as P } from 'react'; - -class Pure extends P { - render() { return
    ; } -} diff --git a/codemods/pure-component/tests/named-import-purecomponent-alias/metrics.json b/codemods/pure-component/tests/named-import-purecomponent-alias/metrics.json deleted file mode 100644 index efaa063..0000000 --- a/codemods/pure-component/tests/named-import-purecomponent-alias/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/named-import-purecomponent-alias/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/tests/react-native-named-import/expected.tsx b/codemods/pure-component/tests/react-native-named-import/expected.tsx deleted file mode 100644 index 6901793..0000000 --- a/codemods/pure-component/tests/react-native-named-import/expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import 'react-native'; - -function Pure(props) { return
    ; } diff --git a/codemods/pure-component/tests/react-native-named-import/input.tsx b/codemods/pure-component/tests/react-native-named-import/input.tsx deleted file mode 100644 index 56e7352..0000000 --- a/codemods/pure-component/tests/react-native-named-import/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Component } from 'react-native'; - -class Pure extends Component { - render() { return
    ; } -} diff --git a/codemods/pure-component/tests/react-native-named-import/metrics.json b/codemods/pure-component/tests/react-native-named-import/metrics.json deleted file mode 100644 index 419d2d6..0000000 --- a/codemods/pure-component/tests/react-native-named-import/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-component-conversions": [ - { - "cardinality": { - "file": "tests/react-native-named-import/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-component/workflow.yaml b/codemods/pure-component/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/pure-component/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/pure-render-mixin/README.md b/codemods/pure-render-mixin/README.md deleted file mode 100644 index 847c05c..0000000 --- a/codemods/pure-render-mixin/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# @react/pure-render-mixin - -Replace `PureRenderMixin` with `shouldComponentUpdate` using `React.addons.shallowCompare`. - -## Usage - -```bash -codemod run @react/pure-render-mixin -``` - -## Options - -- `mixin-name`: custom mixin identifier to replace. Default: `PureRenderMixin`. -- `explicit-require`: when `false`, run even if no React import/require is present. Default: `true`. - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/pure-render-mixin/codemod.yaml b/codemods/pure-render-mixin/codemod.yaml deleted file mode 100644 index 554d2ef..0000000 --- a/codemods/pure-render-mixin/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/pure-render-mixin" -version: "0.1.0" -description: "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/pure-render-mixin/package.json b/codemods/pure-render-mixin/package.json deleted file mode 100644 index 83daaf7..0000000 --- a/codemods/pure-render-mixin/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@react/pure-render-mixin", - "version": "0.1.0", - "description": "Replace PureRenderMixin with shouldComponentUpdate using React.addons.shallowCompare", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "typescript": "latest", - "@types/node": "latest" - } -} diff --git a/codemods/pure-render-mixin/scripts/codemod.ts b/codemods/pure-render-mixin/scripts/codemod.ts deleted file mode 100644 index bf6180d..0000000 --- a/codemods/pure-render-mixin/scripts/codemod.ts +++ /dev/null @@ -1,244 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -const REACT_MODULES = new Set(["React", "react", "react/addons", "react-native"]); - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -function stringValue(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function callArguments(call: SgNode): SgNode[] { - const args = call.field("arguments"); - if (!args) return []; - return args.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function requireSource(call: SgNode): string | null { - const callee = call.field("function"); - if (!callee || callee.kind() !== "identifier" || callee.text() !== "require") return null; - const firstArg = callArguments(call)[0]; - return firstArg?.kind() === "string" ? stringValue(firstArg) : null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? stringValue(source) : null; -} - -function hasReact(rootNode: SgNode): boolean { - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (REACT_MODULES.has(importSource(importNode) ?? "")) return true; - } - - for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - if (REACT_MODULES.has(requireSource(call) ?? "")) return true; - } - - return false; -} - -function explicitRequireDisabled(value: unknown): boolean { - return value === false || value === "false"; -} - -function isReactCreateClassCall(node: SgNode | null): node is SgNode { - if (!node || node.kind() !== "call_expression") return false; - const callee = node.field("function"); - return callee?.kind() === "member_expression" && - callee.field("object")?.kind() === "identifier" && - callee.field("object")?.text() === "React" && - callee.field("property")?.kind() === "property_identifier" && - callee.field("property")?.text() === "createClass"; -} - -function reactCreateClassDeclarators(rootNode: SgNode): Array<{ - declarator: SgNode; - call: SgNode; -}> { - const result: Array<{ declarator: SgNode; call: SgNode }> = []; - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const value = declarator.field("value"); - if (isReactCreateClassCall(value)) result.push({ declarator, call: value }); - } - return result; -} - - -const DEFAULT_MIXIN_NAME = "PureRenderMixin"; -const SHOULD_COMPONENT_UPDATE = "shouldComponentUpdate"; -const RENDER = "render"; - -function getConfigObject(call: SgNode): SgNode | null { - const args = call.field("arguments"); - if (!args) return null; - return args.find({ rule: { kind: "object" } }); -} - -function pairWithKeyRule(keyName: string) { - const escaped = keyName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return { - kind: "pair" as const, - has: { - field: "key", - any: [ - { kind: "property_identifier" as const, regex: `^${escaped}$` }, - { kind: "identifier" as const, regex: `^${escaped}$` }, - ], - }, - }; -} - -const PAIR_RULE = { rule: { kind: "pair" as const } }; - -function arrayIdentifierNamedRule(name: string) { - const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - return { rule: { kind: "identifier" as const, regex: `^${escaped}$` } }; -} - -function getPairKeyName(node: SgNode): string { - return node.field("key")?.text() ?? node.field("name")?.text() ?? ""; -} - -function declarationRemovalRange(varDecl: SgNode, source: string): { start: number; end: number } { - let start = varDecl.range().start.index; - let end = varDecl.range().end.index; - const before = source.slice(0, start); - const after = source.slice(end); - if (after.startsWith("\n\n")) { - end += 1; - } - if (before.endsWith("\n\n") && after.startsWith("\n\n")) { - start -= 1; - } - return { start, end }; -} - -function shouldUpdatePropertyText(): string { - return `${SHOULD_COMPONENT_UPDATE}: function(nextProps, nextState) {\n return React.addons.shallowCompare(this, nextProps, nextState);\n }`; -} - -function renderConfigObject(configObj: SgNode, mixinsPair: SgNode, mixinName: string): string | null { - const mixinsValue = mixinsPair.field("value"); - if (!mixinsValue || mixinsValue.kind() !== "array") return null; - const mixinInArray = mixinsValue.findAll(arrayIdentifierNamedRule(mixinName)); - if (mixinInArray.length === 0) return null; - - const toRemoveSet = new Set(mixinInArray.map((n) => n.range().start.index)); - const kept = mixinsValue.children().filter( - (c) => - c.kind() !== "[" && - c.kind() !== "]" && - c.kind() !== "," && - !toRemoveSet.has(c.range().start.index), - ); - - const pairs = configObj.findAll(PAIR_RULE) - .sort((a, b) => a.range().start.index - b.range().start.index); - const renderedProps: string[] = []; - let renderIndex = -1; - - for (const pair of pairs) { - if (pair.id() === mixinsPair.id()) { - if (kept.length > 0) { - renderedProps.push(`mixins: [${kept.map((e) => e.text()).join(", ")}]`); - } - continue; - } - - if (getPairKeyName(pair) === RENDER) { - renderIndex = renderedProps.length; - } - renderedProps.push(pair.text()); - } - - if (renderedProps.length === 0) return null; - - const shouldUpdateText = shouldUpdatePropertyText(); - if (renderIndex >= 0 && renderIndex === renderedProps.length - 1) { - renderedProps.splice(renderIndex, 0, shouldUpdateText); - } else { - renderedProps.push(shouldUpdateText); - } - - return `{\n ${renderedProps.join(",\n\n ")},\n}`; -} - -const transform: Transform = async (root, options) => { - const rootNode = root.root(); - if (!explicitRequireDisabled(options.params?.["explicit-require"]) && !hasReact(rootNode)) { - return null; - } - - const source = rootNode.text(); - const edits: Edit[] = []; - - const mixinName = (options.params?.["mixin-name"] as string) ?? DEFAULT_MIXIN_NAME; - const metric = useMetricAtom("pure-render-mixin-replacements"); - let transformCount = 0; - - for (const { call } of reactCreateClassDeclarators(rootNode)) { - const configObj = getConfigObject(call); - if (!configObj) continue; - - const mixinsPair = configObj.find({ rule: pairWithKeyRule("mixins") }); - const hasShouldUpdate = configObj.find({ rule: pairWithKeyRule(SHOULD_COMPONENT_UPDATE) }) !== null; - const renderPair = configObj.find({ rule: pairWithKeyRule(RENDER) }); - - if (!mixinsPair || hasShouldUpdate || !renderPair) continue; - - const value = mixinsPair.field("value"); - if (!value || value.kind() !== "array") continue; - const mixinInArray = value.findAll(arrayIdentifierNamedRule(mixinName)); - if (mixinInArray.length === 0) continue; - - const newConfigText = renderConfigObject(configObj, mixinsPair, mixinName); - if (!newConfigText) continue; - edits.push(configObj.replace(newConfigText)); - - transformCount++; - metric.increment({ file: metricFile(root.filename()) }); - } - - if (edits.length > 0 && transformCount > 0) { - const escapedMixin = mixinName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const mixinVarDeclarators = rootNode.findAll({ - rule: { - kind: "variable_declarator", - has: { field: "name", kind: "identifier" as const, regex: `^${escapedMixin}$` }, - }, - }); - const mixinIdentifiers = rootNode.findAll({ - rule: { kind: "identifier" as const, regex: `^${escapedMixin}$` }, - }); - if (mixinIdentifiers.length <= 2) { - for (const decl of mixinVarDeclarators) { - const varDecl = decl.parent(); - if (!varDecl || varDecl.kind() !== "variable_declaration") continue; - const decls = varDecl.findAll({ rule: { kind: "variable_declarator" } }); - if (decls.length !== 1) continue; - const after = source.slice(varDecl.range().end.index, varDecl.range().end.index + 10); - const range = declarationRemovalRange(varDecl, source); - edits.push({ - startPos: range.start, - endPos: range.end, - insertedText: "", - }); - break; - } - } - } - - if (edits.length === 0) return null; - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx deleted file mode 100644 index bd81dcc..0000000 --- a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; -var createClass = require('create-react-class'); - -var C = createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx b/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx deleted file mode 100644 index bd81dcc..0000000 --- a/codemods/pure-render-mixin/tests/identifier-create-class-no-change/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -var React = require('react/addons'); -var PureRenderMixin = React.addons.PureRenderMixin; -var createClass = require('create-react-class'); - -var C = createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx b/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx deleted file mode 100644 index 47bd5ed..0000000 --- a/codemods/pure-render-mixin/tests/no-react-explicit-require/expected.tsx +++ /dev/null @@ -1,10 +0,0 @@ - -var C = React.createClass({ - shouldComponentUpdate: function(nextProps, nextState) { - return React.addons.shallowCompare(this, nextProps, nextState); - }, - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx b/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx deleted file mode 100644 index 13cd748..0000000 --- a/codemods/pure-render-mixin/tests/no-react-explicit-require/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var PureRenderMixin = React.addons.PureRenderMixin; - -var C = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json b/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json deleted file mode 100644 index fd0f9ae..0000000 --- a/codemods/pure-render-mixin/tests/no-react-explicit-require/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/no-react-explicit-require/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json b/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json deleted file mode 100644 index 4858435..0000000 --- a/codemods/pure-render-mixin/tests/no-react-explicit-require/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "explicit-require": false - } -} diff --git a/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx b/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx deleted file mode 100644 index 13cd748..0000000 --- a/codemods/pure-render-mixin/tests/no-react-no-change/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var PureRenderMixin = React.addons.PureRenderMixin; - -var C = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx b/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx deleted file mode 100644 index 13cd748..0000000 --- a/codemods/pure-render-mixin/tests/no-react-no-change/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var PureRenderMixin = React.addons.PureRenderMixin; - -var C = React.createClass({ - mixins: [PureRenderMixin], - - render: function() { - return
    ; - }, -}); diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin/metrics.json b/codemods/pure-render-mixin/tests/pure-render-mixin/metrics.json deleted file mode 100644 index 9abd990..0000000 --- a/codemods/pure-render-mixin/tests/pure-render-mixin/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/pure-render-mixin/input.tsx" - }, - "count": 3 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin2/metrics.json b/codemods/pure-render-mixin/tests/pure-render-mixin2/metrics.json deleted file mode 100644 index 40a2653..0000000 --- a/codemods/pure-render-mixin/tests/pure-render-mixin2/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/pure-render-mixin2/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin3/metrics.json b/codemods/pure-render-mixin/tests/pure-render-mixin3/metrics.json deleted file mode 100644 index 90c32c0..0000000 --- a/codemods/pure-render-mixin/tests/pure-render-mixin3/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/pure-render-mixin3/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin4/metrics.json b/codemods/pure-render-mixin/tests/pure-render-mixin4/metrics.json deleted file mode 100644 index 2f39039..0000000 --- a/codemods/pure-render-mixin/tests/pure-render-mixin4/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "pure-render-mixin-replacements": [ - { - "cardinality": { - "file": "tests/pure-render-mixin4/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/pure-render-mixin/tests/pure-render-mixin4/test.config.json b/codemods/pure-render-mixin/tests/pure-render-mixin4/test.config.json deleted file mode 100644 index b9f5c63..0000000 --- a/codemods/pure-render-mixin/tests/pure-render-mixin4/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "mixin-name": "ReactComponentWithPureRenderMixin" - } -} diff --git a/codemods/pure-render-mixin/workflow.yaml b/codemods/pure-render-mixin/workflow.yaml deleted file mode 100644 index 0f8367a..0000000 --- a/codemods/pure-render-mixin/workflow.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -params: - schema: - mixin-name: - name: "Mixin Name" - description: "Identifier name for the pure render mixin to replace." - type: string - default: "PureRenderMixin" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/react-dom-to-react-dom-factories/README.md b/codemods/react-dom-to-react-dom-factories/README.md deleted file mode 100644 index 09f7e8e..0000000 --- a/codemods/react-dom-to-react-dom-factories/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/react-dom-to-react-dom-factories - -Replace `React.DOM.*(...)` and `DOM.*(...)` factory calls with `createElement(...)`. - -## Usage - -```bash -codemod run @react/react-dom-to-react-dom-factories -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/react-dom-to-react-dom-factories/codemod.yaml b/codemods/react-dom-to-react-dom-factories/codemod.yaml deleted file mode 100644 index 51748da..0000000 --- a/codemods/react-dom-to-react-dom-factories/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/react-dom-to-react-dom-factories" -version: "0.1.0" -description: "Replace React.DOM factories with createElement calls" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React", "DOM"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/react-dom-to-react-dom-factories/package.json b/codemods/react-dom-to-react-dom-factories/package.json deleted file mode 100644 index 1c19476..0000000 --- a/codemods/react-dom-to-react-dom-factories/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@react/react-dom-to-react-dom-factories", - "version": "0.1.0", - "description": "Replace React.DOM factories with createElement calls", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "@types/node": "latest", - "typescript": "latest" - }, - "dependencies": { - "@jssg/utils": "^0.0.2" - } -} diff --git a/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts b/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts deleted file mode 100644 index 06dd5ed..0000000 --- a/codemods/react-dom-to-react-dom-factories/scripts/codemod.ts +++ /dev/null @@ -1,173 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; -import { getImport } from "@jssg/utils/javascript/imports"; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -function namedChildren(node: SgNode | null): SgNode[] { - if (!node) { - return []; - } - - return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function stringValue(node: SgNode): string { - const evaluated = Function(`"use strict"; return (${node.text()});`)() as unknown; - if (typeof evaluated !== "string") { - throw new Error(`Expected string literal, got "${typeof evaluated}"`); - } - - return evaluated; -} - -function callArguments(call: SgNode): SgNode[] { - return namedChildren(call.field("arguments")); -} - -function isReactRequireCall(node: SgNode | null): boolean { - if (!node || node.kind() !== "call_expression") { - return false; - } - - const callee = node.field("function"); - const firstArg = callArguments(node)[0]; - return callee?.kind() === "identifier" && - callee.text() === "require" && - firstArg?.kind() === "string" && - stringValue(firstArg) === "react"; -} - -function isReactIdentifier(node: SgNode | null): boolean { - return node?.kind() === "identifier" && node.text() === "React"; -} - -function isReactDomFactoryMember(callee: SgNode): { factory: string; target: string } | null { - if (callee.kind() !== "member_expression") { - return null; - } - - const property = callee.field("property"); - const object = callee.field("object"); - if (!property || !object || property.kind() !== "property_identifier") { - return null; - } - - if (object.kind() === "identifier" && object.text() === "DOM") { - return { factory: property.text(), target: "createElement" }; - } - - if (object.kind() !== "member_expression") { - return null; - } - - const outerProperty = object.field("property"); - const outerObject = object.field("object"); - if (!outerProperty || !outerObject) { - return null; - } - - if (outerProperty.text() === "DOM" && isReactIdentifier(outerObject)) { - return { factory: property.text(), target: "React.createElement" }; - } - - return null; -} - -function isReactDomDestructurePattern(node: SgNode): boolean { - if (node.kind() !== "object_pattern") { - return false; - } - - const declarator = node.parent(); - if (!declarator || declarator.kind() !== "variable_declarator") { - return false; - } - - const init = declarator.field("value"); - if (!(isReactIdentifier(init) || isReactRequireCall(init))) { - return false; - } - - return node.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) - .some((property) => property.text() === "DOM"); -} - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const edits: Edit[] = []; - const metric = useMetricAtom("react-dom-factory-replacements"); - let callReplacements = 0; - - const reactDomImport = getImport(rootNode, { type: "named", name: "DOM", from: "react" }); - const hasReactDomPattern = rootNode.findAll({ rule: { kind: "object_pattern" } }) - .some(isReactDomDestructurePattern); - const hasReactDomBinding = reactDomImport !== null || hasReactDomPattern; - - for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - const callee = call.field("function"); - if (!callee) { - continue; - } - - const match = isReactDomFactoryMember(callee); - if (!match) { - continue; - } - - if (match.target === "createElement" && !hasReactDomBinding) { - continue; - } - - const args = callArguments(call); - const argsText = args.map((arg) => arg.text()).join(", "); - const prefix = argsText.length > 0 ? ", " : ""; - edits.push(call.replace(`${match.target}('${match.factory}'${prefix}${argsText})`)); - callReplacements++; - } - - for (const importSpec of rootNode.findAll({ rule: { kind: "import_specifier" } })) { - const imported = importSpec.field("name"); - if (!imported || imported.text() !== "DOM") { - continue; - } - - const importStmt = importSpec.ancestors().find((ancestor) => ancestor.kind() === "import_statement"); - if (!importStmt) { - continue; - } - - const source = importStmt.field("source"); - if (!source || source.kind() !== "string" || stringValue(source) !== "react") { - continue; - } - - edits.push(imported.replace("createElement")); - } - - for (const shorthand of rootNode.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } })) { - if (shorthand.text() !== "DOM") { - continue; - } - - const pattern = shorthand.ancestors().find((ancestor) => ancestor.kind() === "object_pattern"); - if (!pattern || !isReactDomDestructurePattern(pattern)) { - continue; - } - - edits.push(shorthand.replace("createElement")); - } - - if (edits.length === 0) { - return null; - } - - metric.increment({ file: metricFile(root.filename()) }, callReplacements); - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/metrics.json deleted file mode 100644 index 6b0f717..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-basic-case/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-dom-factory-replacements": [ - { - "cardinality": { - "file": "tests/react-dom-basic-case/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/metrics.json deleted file mode 100644 index a44f433..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-import/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-dom-factory-replacements": [ - { - "cardinality": { - "file": "tests/react-dom-deconstructed-import/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/metrics.json deleted file mode 100644 index 8528c73..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require-part-two/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-dom-factory-replacements": [ - { - "cardinality": { - "file": "tests/react-dom-deconstructed-require-part-two/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/metrics.json b/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/metrics.json deleted file mode 100644 index 437351b..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-deconstructed-require/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-dom-factory-replacements": [ - { - "cardinality": { - "file": "tests/react-dom-deconstructed-require/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/input.tsx deleted file mode 100644 index b7657cf..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import-dom-from-other-libraries/input.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import {DOM} from 'Free'; - -const foo = DOM.div('a', 'b', 'c'); -const bar = Free.DOM.div('a', 'b', 'c'); diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/input.tsx deleted file mode 100644 index 3c7cf3f..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-import/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; - -class Hello extends React.Component { - render() { - return React.createElement('div', null, `Hello ${this.props.toWhat}`); - } -} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/input.tsx deleted file mode 100644 index 3cf194d..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-local-dom-from-other-libraries/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -DOM = 'this is a test!'; - -foo.DOM = {}; - -foo.DOM.div = () => null; - -const bar = foo.DOM.div('a', 'b', 'c'); \ No newline at end of file diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/expected.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/expected.tsx deleted file mode 100644 index 0cdce6e..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/expected.tsx +++ /dev/null @@ -1,8 +0,0 @@ -const React = require('react'); -const { Component } = React; - -class Hello extends Component { - render() { - return DOM.div(null, `Hello ${this.props.toWhat}`); - } -} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/input.tsx deleted file mode 100644 index 0cdce6e..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-react-destructure-without-dom/input.tsx +++ /dev/null @@ -1,8 +0,0 @@ -const React = require('react'); -const { Component } = React; - -class Hello extends Component { - render() { - return DOM.div(null, `Hello ${this.props.toWhat}`); - } -} diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/input.tsx deleted file mode 100644 index 14a21a0..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require-dom-from-other-libraries/input.tsx +++ /dev/null @@ -1,4 +0,0 @@ -const {DOM} = require('Free'); - -const foo = DOM.div('a', 'b', 'c'); -const bar = Free.DOM.div('a', 'b', 'c'); diff --git a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/input.tsx b/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/input.tsx deleted file mode 100644 index fdc8bc6..0000000 --- a/codemods/react-dom-to-react-dom-factories/tests/react-dom-no-change-require/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -const React = require('react'); - -class Hello extends React.Component { - render() { - return React.createElement('div', null, `Hello ${this.props.toWhat}`); - } -} diff --git a/codemods/react-dom-to-react-dom-factories/workflow.yaml b/codemods/react-dom-to-react-dom-factories/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/react-dom-to-react-dom-factories/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/react-native-view-prop-types/README.md b/codemods/react-native-view-prop-types/README.md deleted file mode 100644 index d456bb2..0000000 --- a/codemods/react-native-view-prop-types/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/react-native-view-prop-types - -Replace `View.propTypes` with `ViewPropTypes`. - -## Usage - -```bash -codemod run @react/react-native-view-prop-types -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/react-native-view-prop-types/codemod.yaml b/codemods/react-native-view-prop-types/codemod.yaml deleted file mode 100644 index 3eee70e..0000000 --- a/codemods/react-native-view-prop-types/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/react-native-view-prop-types" -version: "0.1.0" -description: "Replace View.propTypes with ViewPropTypes" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React Native", "View"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/react-native-view-prop-types/package.json b/codemods/react-native-view-prop-types/package.json deleted file mode 100644 index 694e514..0000000 --- a/codemods/react-native-view-prop-types/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@react/react-native-view-prop-types", - "version": "0.1.0", - "description": "Replace View.propTypes with ViewPropTypes", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "@types/node": "latest", - "typescript": "latest" - }, - "dependencies": { - "@jssg/utils": "^0.0.2" - } -} diff --git a/codemods/react-native-view-prop-types/scripts/codemod.ts b/codemods/react-native-view-prop-types/scripts/codemod.ts deleted file mode 100644 index 6cb0d07..0000000 --- a/codemods/react-native-view-prop-types/scripts/codemod.ts +++ /dev/null @@ -1,263 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -function namedChildren(node: SgNode | null): SgNode[] { - if (!node) { - return []; - } - - return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function callArguments(call: SgNode): SgNode[] { - return namedChildren(call.field("arguments")); -} - -function stringValue(node: SgNode): string { - const evaluated = Function(`"use strict"; return (${node.text()});`)() as unknown; - if (typeof evaluated !== "string") { - throw new Error(`Expected string literal, got "${typeof evaluated}"`); - } - - return evaluated; -} - -function isRequireCall(node: SgNode | null, source: string): boolean { - if (!node || node.kind() !== "call_expression") { - return false; - } - - const callee = node.field("function"); - const firstArg = callArguments(node)[0]; - return callee?.kind() === "identifier" && - callee.text() === "require" && - firstArg?.kind() === "string" && - stringValue(firstArg) === source; -} - -function isViewPropTypesAccess(node: SgNode): boolean { - if (node.kind() !== "member_expression") { - return false; - } - - const object = node.field("object"); - const property = node.field("property"); - return object?.kind() === "identifier" && - object.text() === "View" && - property?.kind() === "property_identifier" && - property.text() === "propTypes"; -} - -function isViewBindingNode(node: SgNode): boolean { - if (node.text() !== "View") { - return false; - } - - if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) { - return false; - } - - const parent = node.parent(); - if (!parent) { - return false; - } - - if (parent.kind() === "member_expression" && - parent.field("object")?.id() === node.id() && - parent.field("property")?.text() === "propTypes") { - return false; - } - - if (parent.kind() === "member_expression" && parent.field("property")?.id() === node.id()) { - return false; - } - - if (parent.kind() === "variable_declarator" && parent.field("name")?.id() === node.id()) { - return false; - } - - if (parent.kind() === "shorthand_property_identifier_pattern") { - const pattern = parent.ancestors().find((ancestor) => ancestor.kind() === "object_pattern"); - if (pattern?.parent()?.kind() === "variable_declarator") { - return false; - } - } - - return true; -} - -function findHasteImportStatement(rootNode: SgNode): SgNode | null { - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - const source = importStmt.field("source"); - if (source?.kind() === "string" && stringValue(source) === "View") { - return importStmt; - } - } - - return null; -} - -function findHasteRequireStatement(rootNode: SgNode): SgNode | null { - for (const declaration of rootNode.findAll({ - rule: { - any: [ - { pattern: "const View = require('View')" }, - { pattern: "let View = require('View')" }, - { pattern: "var View = require('View')" }, - ], - }, - })) { - if (declaration.kind() === "lexical_declaration" || declaration.kind() === "variable_declaration") { - return declaration; - } - } - - return null; -} - -function findReactNativeImportStatement(rootNode: SgNode): SgNode | null { - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - const source = importStmt.field("source"); - if (source?.kind() === "string" && stringValue(source) === "react-native") { - const hasView = importStmt.findAll({ rule: { kind: "import_specifier" } }) - .some((specifier) => specifier.field("name")?.text() === "View"); - if (hasView) { - return importStmt; - } - } - } - - return null; -} - -function findReactNativeRequireStatement(rootNode: SgNode): SgNode | null { - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const name = declarator.field("name"); - const value = declarator.field("value"); - if (name?.kind() !== "object_pattern" || !isRequireCall(value, "react-native")) { - continue; - } - - const hasView = name.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) - .some((property) => property.text() === "View"); - if (hasView) { - return declarator.ancestors().find((ancestor) => - ancestor.kind() === "lexical_declaration" || ancestor.kind() === "variable_declaration" - ) ?? null; - } - } - - return null; -} - -function viewImportSpecifier(importStmt: SgNode): SgNode | null { - return importStmt.findAll({ rule: { kind: "import_specifier" } }) - .find((specifier) => specifier.field("name")?.text() === "View") ?? null; -} - -function viewRequireProperty(statement: SgNode): SgNode | null { - return statement.findAll({ rule: { kind: "shorthand_property_identifier_pattern" } }) - .find((property) => property.text() === "View") ?? null; -} - -function statementInsertionIndex(source: string, node: SgNode): number { - let index = node.range().end.index; - while (index < source.length && (source[index] === ";" || source[index] === "\r" || source[index] === "\n")) { - index++; - } - return index; -} - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const source = rootNode.text(); - const edits: Edit[] = []; - const metric = useMetricAtom("react-native-view-prop-types-replacements"); - - const hasteImportStatement = findHasteImportStatement(rootNode); - const hasteRequireStatement = findHasteRequireStatement(rootNode); - const reactNativeImportStatement = findReactNativeImportStatement(rootNode); - const reactNativeRequireStatement = findReactNativeRequireStatement(rootNode); - - let replacements = 0; - for (const member of rootNode.findAll({ rule: { kind: "member_expression" } })) { - if (!isViewPropTypesAccess(member)) { - continue; - } - - edits.push(member.replace("ViewPropTypes")); - replacements++; - } - - if (replacements === 0) { - return null; - } - - const keepViewBinding = rootNode.findAll({ - rule: { - any: [ - { kind: "identifier" }, - { kind: "property_identifier" }, - ], - }, - }).some(isViewBindingNode); - - if (hasteImportStatement) { - if (keepViewBinding) { - edits.push({ - startPos: hasteImportStatement.range().end.index, - endPos: hasteImportStatement.range().end.index, - insertedText: "\nimport ViewPropTypes from 'ViewPropTypes';", - }); - } else { - edits.push(hasteImportStatement.replace("import ViewPropTypes from 'ViewPropTypes';")); - } - } else if (hasteRequireStatement) { - if (keepViewBinding) { - edits.push({ - startPos: statementInsertionIndex(source, hasteRequireStatement), - endPos: statementInsertionIndex(source, hasteRequireStatement), - insertedText: "\nconst ViewPropTypes = require('ViewPropTypes');\n", - }); - } else { - edits.push(hasteRequireStatement.replace("const ViewPropTypes = require('ViewPropTypes');")); - } - } else if (reactNativeImportStatement) { - const specifier = viewImportSpecifier(reactNativeImportStatement); - if (specifier) { - if (keepViewBinding) { - edits.push({ - startPos: specifier.range().end.index, - endPos: specifier.range().end.index, - insertedText: ", ViewPropTypes", - }); - } else { - edits.push(specifier.replace("ViewPropTypes")); - } - } - } else if (reactNativeRequireStatement) { - const property = viewRequireProperty(reactNativeRequireStatement); - if (property) { - if (keepViewBinding) { - edits.push({ - startPos: property.range().end.index, - endPos: property.range().end.index, - insertedText: ", ViewPropTypes", - }); - } else { - edits.push(property.replace("ViewPropTypes")); - } - } - } - - metric.increment({ file: metricFile(root.filename()) }, replacements); - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/react-native-view-prop-types/tests/default-import-multi-reference/metrics.json b/codemods/react-native-view-prop-types/tests/default-import-multi-reference/metrics.json deleted file mode 100644 index e59f9f4..0000000 --- a/codemods/react-native-view-prop-types/tests/default-import-multi-reference/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/default-import-multi-reference/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/default-import-only-reference/metrics.json b/codemods/react-native-view-prop-types/tests/default-import-only-reference/metrics.json deleted file mode 100644 index f298b7d..0000000 --- a/codemods/react-native-view-prop-types/tests/default-import-only-reference/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/default-import-only-reference/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/default-require-multi-reference/metrics.json b/codemods/react-native-view-prop-types/tests/default-require-multi-reference/metrics.json deleted file mode 100644 index 8eebc32..0000000 --- a/codemods/react-native-view-prop-types/tests/default-require-multi-reference/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/default-require-multi-reference/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/default-require-only-reference/metrics.json b/codemods/react-native-view-prop-types/tests/default-require-only-reference/metrics.json deleted file mode 100644 index 6c4cdbe..0000000 --- a/codemods/react-native-view-prop-types/tests/default-require-only-reference/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/default-require-only-reference/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/metrics.json b/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/metrics.json deleted file mode 100644 index f9bfb2c..0000000 --- a/codemods/react-native-view-prop-types/tests/destructured-import-multi-reference/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/destructured-import-multi-reference/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/metrics.json b/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/metrics.json deleted file mode 100644 index a530459..0000000 --- a/codemods/react-native-view-prop-types/tests/destructured-import-only-reference/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/destructured-import-only-reference/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/metrics.json b/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/metrics.json deleted file mode 100644 index 2b8657f..0000000 --- a/codemods/react-native-view-prop-types/tests/destructured-require-multi-reference/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/destructured-require-multi-reference/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/metrics.json b/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/metrics.json deleted file mode 100644 index b7af569..0000000 --- a/codemods/react-native-view-prop-types/tests/destructured-require-only-reference/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/destructured-require-only-reference/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/metrics.json b/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/metrics.json deleted file mode 100644 index dbd793c..0000000 --- a/codemods/react-native-view-prop-types/tests/import-flow-type-with-require/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/import-flow-type-with-require/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/multiple-replacements/metrics.json b/codemods/react-native-view-prop-types/tests/multiple-replacements/metrics.json deleted file mode 100644 index 6ad8dcd..0000000 --- a/codemods/react-native-view-prop-types/tests/multiple-replacements/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "react-native-view-prop-types-replacements": [ - { - "cardinality": { - "file": "tests/multiple-replacements/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/react-native-view-prop-types/tests/noop-import/input.tsx b/codemods/react-native-view-prop-types/tests/noop-import/input.tsx deleted file mode 100644 index e02fc4a..0000000 --- a/codemods/react-native-view-prop-types/tests/noop-import/input.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import React from 'React'; -import View from 'View'; - -module.exports = function Component() { - return ; -}; diff --git a/codemods/react-native-view-prop-types/tests/noop-require/input.tsx b/codemods/react-native-view-prop-types/tests/noop-require/input.tsx deleted file mode 100644 index c1bc51a..0000000 --- a/codemods/react-native-view-prop-types/tests/noop-require/input.tsx +++ /dev/null @@ -1,6 +0,0 @@ -const React = require('React'); -const View = require('View'); - -module.exports = function Component() { - return ; -}; diff --git a/codemods/react-native-view-prop-types/tsconfig.json b/codemods/react-native-view-prop-types/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/react-native-view-prop-types/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/react-native-view-prop-types/workflow.yaml b/codemods/react-native-view-prop-types/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/react-native-view-prop-types/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/react-proptypes-to-prop-types/README.md b/codemods/react-proptypes-to-prop-types/README.md deleted file mode 100644 index dc92c06..0000000 --- a/codemods/react-proptypes-to-prop-types/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# @react/react-proptypes-to-prop-types - -Replace legacy React `PropTypes` usage with the `prop-types` package across imports, requires, aliases, and destructuring patterns. - -## Usage - -```bash -codemod run @react/react-proptypes-to-prop-types -``` - -## Options - -- `module-name`: module specifier to import or require for `PropTypes`. Default: `prop-types`. - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/react-proptypes-to-prop-types/tsconfig.json b/codemods/react-proptypes-to-prop-types/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/react-proptypes-to-prop-types/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/react-to-react-dom/README.md b/codemods/react-to-react-dom/README.md deleted file mode 100644 index 94db7a8..0000000 --- a/codemods/react-to-react-dom/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/react-to-react-dom - -Move DOM APIs from `react` to `react-dom` and `react-dom/server`. - -## Usage - -```bash -codemod run @react/react-to-react-dom -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/react-to-react-dom/codemod.yaml b/codemods/react-to-react-dom/codemod.yaml deleted file mode 100644 index 7d096f6..0000000 --- a/codemods/react-to-react-dom/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/react-to-react-dom" -version: "0.1.0" -description: "Move DOM APIs from react to react-dom" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React", "ReactDOM"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/react-to-react-dom/package.json b/codemods/react-to-react-dom/package.json deleted file mode 100644 index e888dc4..0000000 --- a/codemods/react-to-react-dom/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@react/react-to-react-dom", - "version": "0.1.0", - "description": "Move DOM APIs from react to react-dom", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose && node --test ./scripts/error-tests.mjs", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "@types/node": "latest", - "typescript": "latest" - } -} diff --git a/codemods/react-to-react-dom/scripts/codemod.ts b/codemods/react-to-react-dom/scripts/codemod.ts deleted file mode 100644 index 102ece7..0000000 --- a/codemods/react-to-react-dom/scripts/codemod.ts +++ /dev/null @@ -1,713 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -type NamedSpecifier = { - name: string; - alias?: string; -}; - -type ImportBinding = { - alias: string | null; - named: NamedSpecifier[]; - statements: SgNode[]; - quote: string; -}; - -type RequireBinding = { - alias: string; - statement: SgNode; - kind: "var" | "let" | "const"; -}; - -type AssignmentBinding = { - alias: string; - declaration: SgNode | null; - assignment: SgNode; - kind: "var" | "let" | "const"; -}; - -type ModulePair = { - core: string; - dom: string; - server: string; - domAlias: string; - serverAlias: string; -}; - -type MemberAnalysis = { - edits: Edit[]; - domUses: number; - serverUses: number; - coreUses: number; -}; - -type DestructureAnalysis = { - edits: Edit[]; - domUses: number; - coreUses: number; -}; - -const CORE_PROPERTIES = [ - "Children", - "Component", - "createElement", - "cloneElement", - "isValidElement", - "PropTypes", - "createClass", - "createFactory", - "createMixin", - "DOM", - "__spread", -] as const; - -const DOM_PROPERTIES = [ - "findDOMNode", - "render", - "unmountComponentAtNode", - "unstable_batchedUpdates", - "unstable_renderSubtreeIntoContainer", -] as const; - -const DOM_SERVER_PROPERTIES = [ - "renderToString", - "renderToStaticMarkup", -] as const; - -const CORE_SET = new Set(CORE_PROPERTIES); -const DOM_SET = new Set(DOM_PROPERTIES); -const DOM_SERVER_SET = new Set(DOM_SERVER_PROPERTIES); - -const MODULE_PAIRS: ModulePair[] = [ - { - core: "React", - dom: "ReactDOM", - server: "ReactDOMServer", - domAlias: "ReactDOM", - serverAlias: "ReactDOMServer", - }, - { - core: "react", - dom: "react-dom", - server: "react-dom/server", - domAlias: "ReactDOM", - serverAlias: "ReactDOMServer", - }, -]; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -function namedChildren(node: SgNode | null): SgNode[] { - if (!node) { - return []; - } - - return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function exactRule(kind: "identifier" | "type_identifier" | "property_identifier", name: string) { - return { - kind, - regex: `^${name}$`, - } as const; -} - -function statementRange(node: SgNode, source: string): { start: number; end: number } { - const range = node.range(); - let end = range.end.index; - while (end < source.length && (source[end] === "\n" || source[end] === "\r")) { - end++; - } - return { start: range.start.index, end }; -} - -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) { - return fragment.text(); - } - - const text = node.text(); - if (text.length >= 2) { - return text.slice(1, -1); - } - - return null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - -function quoteForImport(node: SgNode): string { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - const text = source?.text() ?? "'react'"; - return text.startsWith("\"") ? "\"" : "'"; -} - -function callArguments(call: SgNode): SgNode[] { - return namedChildren(call.field("arguments")); -} - -function isRequireOf(node: SgNode | null, moduleName: string): boolean { - if (!node || node.kind() !== "call_expression") { - return false; - } - - const callee = node.field("function"); - const firstArg = callArguments(node)[0]; - return callee?.kind() === "identifier" && - callee.text() === "require" && - firstArg?.kind() === "string" && - sourceText(firstArg) === moduleName; -} - -function declarationKind(statement: SgNode): "var" | "let" | "const" { - const text = statement.text(); - if (text.startsWith("const ")) return "const"; - if (text.startsWith("let ")) return "let"; - return "var"; -} - -function buildImportLine( - quote: string, - source: string, - defaultAlias: string | null, - named: NamedSpecifier[], -): string { - const parts: string[] = []; - if (defaultAlias) { - parts.push(defaultAlias); - } - if (named.length > 0) { - const rendered = named.map((spec) => - spec.alias && spec.alias !== spec.name ? `${spec.name} as ${spec.alias}` : spec.name - ); - parts.push(`{ ${rendered.join(", ")} }`); - } - return `import ${parts.join(", ")} from ${quote}${source}${quote};`; -} - -function buildRequireLine(kind: "var" | "let" | "const", alias: string, source: string): string { - return `${kind} ${alias} = require('${source}');`; -} - -function buildRequireDeclaration(kind: "var" | "let" | "const", alias: string): string { - return `${kind} ${alias};`; -} - -function buildRequireAssignment(alias: string, source: string): string { - return `${alias} = require('${source}');`; -} - -function uniqueSpecs(specs: NamedSpecifier[]): NamedSpecifier[] { - const seen = new Set(); - const result: NamedSpecifier[] = []; - for (const spec of specs) { - const key = `${spec.name}:${spec.alias ?? ""}`; - if (!seen.has(key)) { - seen.add(key); - result.push(spec); - } - } - return result; -} - -function sortSpecs(specs: NamedSpecifier[]): NamedSpecifier[] { - return [...specs].sort((left, right) => left.name.localeCompare(right.name)); -} - -function findModuleImports(rootNode: SgNode, moduleName: string): ImportBinding { - const statements = rootNode.findAll({ rule: { kind: "import_statement" } }) - .filter((stmt) => importSource(stmt) === moduleName); - - const named: NamedSpecifier[] = []; - let alias: string | null = null; - let quote = "'"; - - for (const stmt of statements) { - quote = quoteForImport(stmt); - const clause = stmt.find({ rule: { kind: "import_clause" } }); - if (!clause) { - continue; - } - - const defaultIdentifier = namedChildren(clause) - .find((child) => child.kind() === "identifier"); - if (defaultIdentifier) { - alias = defaultIdentifier.text(); - } - - for (const spec of stmt.findAll({ rule: { kind: "import_specifier" } })) { - named.push({ - name: spec.field("name")?.text() ?? spec.text(), - alias: spec.field("alias")?.text() ?? undefined, - }); - } - } - - return { - alias, - named: uniqueSpecs(named), - statements, - quote, - }; -} - -function findRequireBinding(rootNode: SgNode, moduleName: string): RequireBinding | null { - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const name = declarator.field("name"); - const value = declarator.field("value"); - if (name?.kind() !== "identifier" || !isRequireOf(value, moduleName)) { - continue; - } - - const statement = declarator.ancestors().find((ancestor) => - ancestor.kind() === "variable_declaration" || ancestor.kind() === "lexical_declaration" - ); - if (!statement) { - continue; - } - - return { - alias: name.text(), - statement, - kind: declarationKind(statement), - }; - } - - return null; -} - -function findAssignmentBinding(rootNode: SgNode, moduleName: string): AssignmentBinding | null { - for (const assignment of rootNode.findAll({ rule: { kind: "assignment_expression" } })) { - const left = assignment.field("left"); - const right = assignment.field("right"); - if (left?.kind() !== "identifier" || !isRequireOf(right, moduleName)) { - continue; - } - - const assignmentStatement = assignment.ancestors().find((ancestor) => ancestor.kind() === "expression_statement"); - if (!assignmentStatement) { - continue; - } - - const alias = left.text(); - const bareDeclaration = rootNode.findAll({ rule: { kind: "variable_declarator" } }) - .find((declarator) => declarator.field("name")?.text() === alias && !declarator.field("value")); - const declaration = bareDeclaration?.ancestors().find((ancestor) => - ancestor.kind() === "variable_declaration" || ancestor.kind() === "lexical_declaration" - ) ?? null; - - return { - alias, - declaration, - assignment: assignmentStatement, - kind: declaration ? declarationKind(declaration) : "var", - }; - } - - return null; -} - -function hasCoreSource(rootNode: SgNode, moduleName: string): boolean { - return findModuleImports(rootNode, moduleName).statements.length > 0 || - findRequireBinding(rootNode, moduleName) !== null || - findAssignmentBinding(rootNode, moduleName) !== null; -} - -function activePair(rootNode: SgNode): ModulePair | null { - for (const pair of MODULE_PAIRS) { - if (hasCoreSource(rootNode, pair.core)) { - return pair; - } - } - - return null; -} - -function hasJSX(rootNode: SgNode): boolean { - return rootNode.findAll({ - rule: { - any: [ - { kind: "jsx_element" }, - { kind: "jsx_self_closing_element" }, - ], - }, - }).length > 0; -} - -function isImportContext(node: SgNode): boolean { - return node.ancestors().some((ancestor) => ancestor.kind() === "import_statement"); -} - -function memberPropertyName(node: SgNode): string | null { - if (node.kind() === "member_expression") { - return node.field("property")?.text() ?? null; - } - - if (node.kind() === "nested_type_identifier") { - const children = namedChildren(node); - return children[1]?.text() ?? null; - } - - return null; -} - -function memberObjectNode(node: SgNode): SgNode | null { - if (node.kind() === "member_expression") { - return node.field("object"); - } - - if (node.kind() === "nested_type_identifier") { - return namedChildren(node)[0] ?? null; - } - - return null; -} - -function isIdentifierNode(node: SgNode | null, name: string): boolean { - return node?.kind() === "identifier" && node.text() === name; -} - -function classifyProperty(name: string): "core" | "dom" | "server" | "unknown" { - if (CORE_SET.has(name)) return "core"; - if (DOM_SET.has(name)) return "dom"; - if (DOM_SERVER_SET.has(name)) return "server"; - return "unknown"; -} - -function analyzeReactMembers( - rootNode: SgNode, - reactAlias: string, - domAlias: string, - serverAlias: string, -): MemberAnalysis { - const edits: Edit[] = []; - let domUses = 0; - let serverUses = 0; - let coreUses = 0; - - const nodes = rootNode.findAll({ - rule: { - any: [ - { kind: "member_expression" }, - { kind: "nested_type_identifier" }, - ], - }, - }); - - for (const node of nodes) { - if (node.kind() === "member_expression" && isImportContext(node)) { - continue; - } - - const object = memberObjectNode(node); - const propertyName = memberPropertyName(node); - if (!isIdentifierNode(object, reactAlias) || !propertyName) { - continue; - } - - const category = classifyProperty(propertyName); - if (category === "unknown") { - throw new Error(`Unknown property React.${propertyName}`); - } - - if (category === "core") { - coreUses++; - continue; - } - - edits.push(object.replace(category === "dom" ? domAlias : serverAlias)); - if (category === "dom") { - domUses++; - } else { - serverUses++; - } - } - - return { edits, domUses, serverUses, coreUses }; -} - -function destructuredNames(pattern: SgNode): string[] { - return pattern.findAll({ - rule: { - any: [ - { kind: "shorthand_property_identifier_pattern" }, - { kind: "pair_pattern" }, - ], - }, - }).map((property) => { - if (property.kind() === "shorthand_property_identifier_pattern") { - return property.text(); - } - return property.field("key")?.text() ?? property.text(); - }); -} - -function analyzeReactDestructures( - rootNode: SgNode, - reactAlias: string, - domAlias: string, -): DestructureAnalysis { - const edits: Edit[] = []; - let domUses = 0; - let coreUses = 0; - - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const pattern = declarator.field("name"); - const value = declarator.field("value"); - if (pattern?.kind() !== "object_pattern" || !isIdentifierNode(value, reactAlias)) { - continue; - } - - const core: string[] = []; - const dom: string[] = []; - - for (const name of destructuredNames(pattern)) { - const category = classifyProperty(name); - if (category === "unknown" || category === "server") { - throw new Error(`Unknown property React.${name} while destructuring`); - } - if (category === "core") { - core.push(name); - } else { - dom.push(name); - } - } - - if (dom.length === 0) { - coreUses++; - continue; - } - - if (core.length === 0) { - edits.push(value.replace(domAlias)); - domUses++; - continue; - } - - const statement = declarator.ancestors().find((ancestor) => - ancestor.kind() === "variable_declaration" || ancestor.kind() === "lexical_declaration" - ); - if (!statement) { - continue; - } - - const declarators = namedChildren(statement).filter((child) => child.kind() === "variable_declarator"); - if (declarators.length !== 1) { - throw new Error("Mixed React destructuring with multiple declarators is unsupported"); - } - - const kind = declarationKind(statement); - edits.push(statement.replace( - `${kind} { ${core.join(", ")} } = ${reactAlias};\n${kind} { ${dom.join(", ")} } = ${domAlias};`, - )); - domUses++; - coreUses++; - } - - return { edits, domUses, coreUses }; -} - -function namedFromReact(binding: ImportBinding, names: readonly string[]): NamedSpecifier[] { - const wanted = new Set(names); - return binding.named.filter((spec) => wanted.has(spec.name)); -} - -function withoutNamed(binding: ImportBinding, names: readonly string[]): NamedSpecifier[] { - const removed = new Set(names); - return binding.named.filter((spec) => !removed.has(spec.name)); -} - -function hasIdentifierReference(rootNode: SgNode, name: string): boolean { - return rootNode.findAll({ - rule: { - any: [ - exactRule("identifier", name), - exactRule("type_identifier", name), - exactRule("property_identifier", name), - ], - }, - }).length > 0; -} - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const source = rootNode.text(); - const metric = useMetricAtom("react-to-react-dom-migrations"); - const edits: Edit[] = []; - - const pair = activePair(rootNode); - if (!pair) { - return null; - } - - const reactImport = findModuleImports(rootNode, pair.core); - const reactDomImport = findModuleImports(rootNode, pair.dom); - const reactDomServerImport = findModuleImports(rootNode, pair.server); - const reactRequire = findRequireBinding(rootNode, pair.core); - const reactAssign = findAssignmentBinding(rootNode, pair.core); - const reactDomRequire = findRequireBinding(rootNode, pair.dom); - const reactDomServerRequire = findRequireBinding(rootNode, pair.server); - - const coreDeclarationCount = reactImport.statements.length + (reactRequire ? 1 : 0) + (reactAssign ? 1 : 0); - if (coreDeclarationCount === 0) { - return null; - } - if (coreDeclarationCount > 1) { - throw new Error("Multiple declarations of React"); - } - - const reactAlias = reactImport.alias ?? reactRequire?.alias ?? reactAssign?.alias ?? null; - const domAlias = reactDomImport.alias ?? reactDomRequire?.alias ?? pair.domAlias; - const serverAlias = reactDomServerImport.alias ?? reactDomServerRequire?.alias ?? pair.serverAlias; - - const memberAnalysis = reactAlias - ? analyzeReactMembers(rootNode, reactAlias, domAlias, serverAlias) - : { edits: [], domUses: 0, serverUses: 0, coreUses: 0 }; - const destructureAnalysis = reactAlias - ? analyzeReactDestructures(rootNode, reactAlias, domAlias) - : { edits: [], domUses: 0, coreUses: 0 }; - - const domNamedFromReact = namedFromReact(reactImport, DOM_PROPERTIES); - const serverNamedFromReact = namedFromReact(reactImport, DOM_SERVER_PROPERTIES); - const coreNamedFromReact = withoutNamed(reactImport, [...DOM_PROPERTIES, ...DOM_SERVER_PROPERTIES]); - - const needsDomBinding = - memberAnalysis.domUses > 0 || - destructureAnalysis.domUses > 0 || - domNamedFromReact.length > 0 || - reactDomImport.statements.length > 0; - const needsServerBinding = - memberAnalysis.serverUses > 0 || - serverNamedFromReact.length > 0 || - reactDomServerImport.statements.length > 0; - - if (needsDomBinding && !reactDomImport.alias && !reactDomRequire && hasIdentifierReference(rootNode, domAlias)) { - throw new Error("ReactDOM is already defined in a different scope than React"); - } - if (needsServerBinding && !reactDomServerImport.alias && !reactDomServerRequire && hasIdentifierReference(rootNode, serverAlias)) { - throw new Error("ReactDOMServer is already defined in a different scope than React"); - } - - edits.push(...memberAnalysis.edits, ...destructureAnalysis.edits); - - if (reactImport.statements.length > 0) { - const quote = reactImport.quote; - const keepReactDefault = reactImport.alias !== null && ( - hasJSX(rootNode) || - memberAnalysis.coreUses > 0 || - destructureAnalysis.coreUses > 0 - ); - - const nextReactNamed = sortSpecs(coreNamedFromReact); - const nextDomNamed = sortSpecs(uniqueSpecs([...reactDomImport.named, ...domNamedFromReact])); - const nextServerNamed = sortSpecs(uniqueSpecs([...reactDomServerImport.named, ...serverNamedFromReact])); - - const lines: string[] = []; - if (keepReactDefault || nextReactNamed.length > 0) { - lines.push(buildImportLine(quote, pair.core, keepReactDefault ? reactImport.alias : null, nextReactNamed)); - } - if (!reactDomRequire && needsDomBinding) { - const defaultAlias = reactDomImport.alias ?? (memberAnalysis.domUses > 0 || destructureAnalysis.domUses > 0 ? domAlias : null); - lines.push(buildImportLine(quote, pair.dom, defaultAlias, nextDomNamed)); - } - if (!reactDomServerRequire && needsServerBinding) { - const defaultAlias = reactDomServerImport.alias ?? (memberAnalysis.serverUses > 0 ? serverAlias : null); - lines.push(buildImportLine(quote, pair.server, defaultAlias, nextServerNamed)); - } - - const allImportStatements = [ - ...reactImport.statements, - ...reactDomImport.statements, - ...reactDomServerImport.statements, - ]; - const ranges = allImportStatements.map((stmt) => statementRange(stmt, source)); - const start = Math.min(...ranges.map((range) => range.start)); - const end = Math.max(...ranges.map((range) => range.end)); - edits.push({ - startPos: start, - endPos: end, - insertedText: lines.length > 0 ? `${lines.join("\n")}\n\n` : "", - }); - } - - if (reactRequire) { - const keepReact = hasJSX(rootNode) || memberAnalysis.coreUses > 0 || destructureAnalysis.coreUses > 0; - const inserted: string[] = []; - if (!reactDomRequire && (memberAnalysis.domUses > 0 || destructureAnalysis.domUses > 0)) { - inserted.push(buildRequireLine(reactRequire.kind, domAlias, pair.dom)); - } - if (!reactDomServerRequire && memberAnalysis.serverUses > 0) { - inserted.push(buildRequireLine(reactRequire.kind, serverAlias, pair.server)); - } - - const range = statementRange(reactRequire.statement, source); - if (keepReact) { - if (inserted.length > 0) { - edits.push({ - startPos: range.start, - endPos: range.end, - insertedText: `${reactRequire.statement.text()}\n\n${inserted.join("\n")}\n\n`, - }); - } - } else { - edits.push({ - startPos: range.start, - endPos: range.end, - insertedText: inserted.length > 0 ? `${inserted.join("\n")}\n` : "", - }); - } - } - - if (reactAssign) { - const declarationLines: string[] = []; - const assignmentLines: string[] = []; - - if (!reactDomRequire && (memberAnalysis.domUses > 0 || destructureAnalysis.domUses > 0)) { - declarationLines.push(buildRequireDeclaration(reactAssign.kind, domAlias)); - assignmentLines.push(buildRequireAssignment(domAlias, pair.dom)); - } - if (!reactDomServerRequire && memberAnalysis.serverUses > 0) { - declarationLines.push(buildRequireDeclaration(reactAssign.kind, serverAlias)); - assignmentLines.push(buildRequireAssignment(serverAlias, pair.server)); - } - - if (declarationLines.length > 0 && reactAssign.declaration) { - const declRange = statementRange(reactAssign.declaration, source); - edits.push({ - startPos: declRange.start, - endPos: declRange.end, - insertedText: `${reactAssign.declaration.text()}\n\n${declarationLines.join("\n")}\n\n`, - }); - } - if (assignmentLines.length > 0) { - const assignRange = statementRange(reactAssign.assignment, source); - edits.push({ - startPos: assignRange.start, - endPos: assignRange.end, - insertedText: `${reactAssign.assignment.text()}\n${assignmentLines.join("\n")}\n`, - }); - } - } - - if (edits.length === 0) { - return null; - } - - metric.increment({ - domMembers: String(memberAnalysis.domUses + destructureAnalysis.domUses + domNamedFromReact.length), - serverMembers: String(memberAnalysis.serverUses + serverNamedFromReact.length), - file: metricFile(root.filename()), - }); - - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/react-to-react-dom/scripts/error-tests.mjs b/codemods/react-to-react-dom/scripts/error-tests.mjs deleted file mode 100644 index 118f9c7..0000000 --- a/codemods/react-to-react-dom/scripts/error-tests.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { mkdtempSync, writeFileSync, rmSync } from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { spawnSync } from "node:child_process"; - -const __filename = fileURLToPath(import.meta.url); -const packageDir = path.dirname(path.dirname(__filename)); -const workflowPath = path.join(packageDir, "workflow.yaml"); - -function runCodemod(input) { - const target = mkdtempSync(path.join(tmpdir(), "react-to-react-dom-")); - const filePath = path.join(target, "input.tsx"); - writeFileSync(filePath, input, "utf8"); - - try { - return spawnSync( - "pnpm", - [ - "dlx", - "codemod@latest", - "workflow", - "run", - "-w", - workflowPath, - "--target", - target, - "--allow-dirty", - ], - { - cwd: packageDir, - encoding: "utf8", - }, - ); - } finally { - rmSync(target, { recursive: true, force: true }); - } -} - -function expectFailure(input, pattern) { - const result = runCodemod(input); - const combined = `${result.stdout}\n${result.stderr}`; - assert.match(combined, pattern); - assert.match(combined, /Failed to execute codemod|InitializationFailed/); -} - -test("throws on conflicting ReactDOM binding", () => { - expectFailure( - "const ReactDOM = 1;\nimport React from 'react';\nReact.render(null);\n", - /ReactDOM is already defined in a different scope than React/, - ); -}); - -test("throws on conflicting ReactDOMServer binding", () => { - expectFailure( - "const ReactDOMServer = 1;\nimport React from 'react';\nReact.renderToString(null);\n", - /ReactDOMServer is already defined in a different scope than React/, - ); -}); - -test("throws on multiple React declarations", () => { - expectFailure( - "import React from 'react';\nimport Foo from 'react';\nReact.render(null);\n", - /Multiple declarations of React/, - ); -}); - -test("throws on unknown React member", () => { - expectFailure( - "import React from 'react';\nconst x = React.Fragment;\n", - /Unknown property React\.Fragment/, - ); -}); - -test("throws on unknown React destructuring", () => { - expectFailure( - "var React;\nReact = require('react');\nlet { Fragment } = React;\n", - /Unknown property React\.Fragment while destructuring/, - ); -}); diff --git a/codemods/react-to-react-dom/tests/import-dom-base-uppercase/expected.tsx b/codemods/react-to-react-dom/tests/import-dom-base-uppercase/expected.tsx deleted file mode 100644 index 8ece2af..0000000 --- a/codemods/react-to-react-dom/tests/import-dom-base-uppercase/expected.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import ReactDOM from 'ReactDOM'; - -ReactDOM.findDOMNode(); -ReactDOM.render(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-base-uppercase/input.tsx b/codemods/react-to-react-dom/tests/import-dom-base-uppercase/input.tsx deleted file mode 100644 index ae38804..0000000 --- a/codemods/react-to-react-dom/tests/import-dom-base-uppercase/input.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'React'; - -React.findDOMNode(); -React.render(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-base-uppercase/metrics.json b/codemods/react-to-react-dom/tests/import-dom-base-uppercase/metrics.json deleted file mode 100644 index be38b7d..0000000 --- a/codemods/react-to-react-dom/tests/import-dom-base-uppercase/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "2", - "file": "tests/import-dom-base-uppercase/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-dom-base/metrics.json b/codemods/react-to-react-dom/tests/import-dom-base/metrics.json deleted file mode 100644 index a191839..0000000 --- a/codemods/react-to-react-dom/tests/import-dom-base/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "6", - "file": "tests/import-dom-base/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-dom-type-identifier/expected.tsx b/codemods/react-to-react-dom/tests/import-dom-type-identifier/expected.tsx deleted file mode 100644 index dd1f21f..0000000 --- a/codemods/react-to-react-dom/tests/import-dom-type-identifier/expected.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import ReactDOM from 'react-dom'; - -type RenderRef = ReactDOM.render; - -ReactDOM.render(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-type-identifier/input.tsx b/codemods/react-to-react-dom/tests/import-dom-type-identifier/input.tsx deleted file mode 100644 index 3decd23..0000000 --- a/codemods/react-to-react-dom/tests/import-dom-type-identifier/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -type RenderRef = React.render; - -React.render(null); diff --git a/codemods/react-to-react-dom/tests/import-dom-type-identifier/metrics.json b/codemods/react-to-react-dom/tests/import-dom-type-identifier/metrics.json deleted file mode 100644 index 3818519..0000000 --- a/codemods/react-to-react-dom/tests/import-dom-type-identifier/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "2", - "file": "tests/import-dom-type-identifier/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-multiple-specifiers/metrics.json b/codemods/react-to-react-dom/tests/import-multiple-specifiers/metrics.json deleted file mode 100644 index afb685e..0000000 --- a/codemods/react-to-react-dom/tests/import-multiple-specifiers/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "2", - "file": "tests/import-multiple-specifiers/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-server-base/metrics.json b/codemods/react-to-react-dom/tests/import-server-base/metrics.json deleted file mode 100644 index 899310c..0000000 --- a/codemods/react-to-react-dom/tests/import-server-base/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "0", - "file": "tests/import-server-base/input.tsx", - "serverMembers": "2" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-with-existing-react-dom/metrics.json b/codemods/react-to-react-dom/tests/import-with-existing-react-dom/metrics.json deleted file mode 100644 index 7767d53..0000000 --- a/codemods/react-to-react-dom/tests/import-with-existing-react-dom/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "2", - "file": "tests/import-with-existing-react-dom/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/import-without-default-specifier/metrics.json b/codemods/react-to-react-dom/tests/import-without-default-specifier/metrics.json deleted file mode 100644 index 289eccc..0000000 --- a/codemods/react-to-react-dom/tests/import-without-default-specifier/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "1", - "file": "tests/import-without-default-specifier/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/metrics.json b/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/metrics.json deleted file mode 100644 index e903a51..0000000 --- a/codemods/react-to-react-dom/tests/mixed-with-existing-react-dom/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "1", - "file": "tests/mixed-with-existing-react-dom/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-dom-base-uppercase/expected.tsx b/codemods/react-to-react-dom/tests/require-dom-base-uppercase/expected.tsx deleted file mode 100644 index 635b207..0000000 --- a/codemods/react-to-react-dom/tests/require-dom-base-uppercase/expected.tsx +++ /dev/null @@ -1,4 +0,0 @@ -var ReactDOM = require('ReactDOM'); - -ReactDOM.findDOMNode(); -ReactDOM.render(null); diff --git a/codemods/react-to-react-dom/tests/require-dom-base-uppercase/input.tsx b/codemods/react-to-react-dom/tests/require-dom-base-uppercase/input.tsx deleted file mode 100644 index 7f949f8..0000000 --- a/codemods/react-to-react-dom/tests/require-dom-base-uppercase/input.tsx +++ /dev/null @@ -1,4 +0,0 @@ -var React = require('React'); - -React.findDOMNode(); -React.render(null); diff --git a/codemods/react-to-react-dom/tests/require-dom-base-uppercase/metrics.json b/codemods/react-to-react-dom/tests/require-dom-base-uppercase/metrics.json deleted file mode 100644 index c46bb29..0000000 --- a/codemods/react-to-react-dom/tests/require-dom-base-uppercase/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "2", - "file": "tests/require-dom-base-uppercase/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-dom-base/metrics.json b/codemods/react-to-react-dom/tests/require-dom-base/metrics.json deleted file mode 100644 index b8c1387..0000000 --- a/codemods/react-to-react-dom/tests/require-dom-base/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "6", - "file": "tests/require-dom-base/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-indirect-mixed/expected.tsx b/codemods/react-to-react-dom/tests/require-indirect-mixed/expected.tsx deleted file mode 100644 index c7e5c74..0000000 --- a/codemods/react-to-react-dom/tests/require-indirect-mixed/expected.tsx +++ /dev/null @@ -1,12 +0,0 @@ -var React; - -var ReactDOM; - -React = require('react'); -ReactDOM = require('react-dom'); -let { Component } = React; -let { render } = ReactDOM; - -class Foo extends Component {} - -render(
    ); diff --git a/codemods/react-to-react-dom/tests/require-indirect-mixed/input.tsx b/codemods/react-to-react-dom/tests/require-indirect-mixed/input.tsx deleted file mode 100644 index 7180a6a..0000000 --- a/codemods/react-to-react-dom/tests/require-indirect-mixed/input.tsx +++ /dev/null @@ -1,8 +0,0 @@ -var React; - -React = require('react'); -let { Component, render } = React; - -class Foo extends Component {} - -render(
    ); diff --git a/codemods/react-to-react-dom/tests/require-indirect-mixed/metrics.json b/codemods/react-to-react-dom/tests/require-indirect-mixed/metrics.json deleted file mode 100644 index de32ab7..0000000 --- a/codemods/react-to-react-dom/tests/require-indirect-mixed/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "1", - "file": "tests/require-indirect-mixed/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-indirect/metrics.json b/codemods/react-to-react-dom/tests/require-indirect/metrics.json deleted file mode 100644 index b9d4a53..0000000 --- a/codemods/react-to-react-dom/tests/require-indirect/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "1", - "file": "tests/require-indirect/input.tsx", - "serverMembers": "0" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-keeps-react/metrics.json b/codemods/react-to-react-dom/tests/require-keeps-react/metrics.json deleted file mode 100644 index 397e979..0000000 --- a/codemods/react-to-react-dom/tests/require-keeps-react/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "1", - "file": "tests/require-keeps-react/input.tsx", - "serverMembers": "1" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tests/require-server-base/metrics.json b/codemods/react-to-react-dom/tests/require-server-base/metrics.json deleted file mode 100644 index 8ad5612..0000000 --- a/codemods/react-to-react-dom/tests/require-server-base/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-to-react-dom-migrations": [ - { - "cardinality": { - "domMembers": "0", - "file": "tests/require-server-base/input.tsx", - "serverMembers": "2" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/react-to-react-dom/tsconfig.json b/codemods/react-to-react-dom/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/react-to-react-dom/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/react-to-react-dom/workflow.yaml b/codemods/react-to-react-dom/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/react-to-react-dom/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/remove-context-provider/README.md b/codemods/remove-context-provider/README.md deleted file mode 100644 index a9ae52c..0000000 --- a/codemods/remove-context-provider/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/remove-context-provider - -Replace `Context.Provider` JSX tags with `Context`. - -## Usage - -```bash -codemod run @react/remove-context-provider -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/remove-context-provider/codemod.yaml b/codemods/remove-context-provider/codemod.yaml deleted file mode 100644 index 1c4b4c9..0000000 --- a/codemods/remove-context-provider/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/remove-context-provider" -version: "0.1.0" -description: "Replace Context.Provider with Context in JSX" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React", "Context"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/remove-context-provider/package.json b/codemods/remove-context-provider/package.json deleted file mode 100644 index 37d7f7d..0000000 --- a/codemods/remove-context-provider/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@react/remove-context-provider", - "version": "0.1.0", - "description": "Replace Context.Provider with Context in JSX", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "@types/node": "latest", - "typescript": "latest" - } -} diff --git a/codemods/remove-context-provider/scripts/codemod.ts b/codemods/remove-context-provider/scripts/codemod.ts deleted file mode 100644 index 4670c2a..0000000 --- a/codemods/remove-context-provider/scripts/codemod.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -function isContextProvider(node: SgNode): boolean { - if (node.kind() !== "member_expression") { - return false; - } - - const object = node.field("object"); - const property = node.field("property"); - if (!object || object.kind() !== "identifier" || !property || property.text() !== "Provider") { - return false; - } - - return object.text().toLowerCase().includes("context"); -} - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const edits: Edit[] = []; - const metric = useMetricAtom("remove-context-provider-replacements"); - let replacements = 0; - - for (const member of rootNode.findAll({ rule: { kind: "member_expression" } })) { - const parent = member.parent(); - if (!parent) { - continue; - } - - const isJsxName = parent.kind() === "jsx_opening_element" || parent.kind() === "jsx_closing_element"; - if (!isJsxName || !isContextProvider(member)) { - continue; - } - - const object = member.field("object"); - if (!object) { - continue; - } - - edits.push(member.replace(object.text())); - replacements++; - } - - if (edits.length === 0) { - return null; - } - - metric.increment({ file: metricFile(root.filename()) }, replacements); - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/remove-context-provider/tests/nested-member-no-change/expected.tsx b/codemods/remove-context-provider/tests/nested-member-no-change/expected.tsx deleted file mode 100644 index 28d0643..0000000 --- a/codemods/remove-context-provider/tests/nested-member-no-change/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -function App() { - return ( - - - - ); -} diff --git a/codemods/remove-context-provider/tests/nested-member-no-change/input.tsx b/codemods/remove-context-provider/tests/nested-member-no-change/input.tsx deleted file mode 100644 index 28d0643..0000000 --- a/codemods/remove-context-provider/tests/nested-member-no-change/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -function App() { - return ( - - - - ); -} diff --git a/codemods/remove-context-provider/tests/typescript-with-provider-2/expected.tsx b/codemods/remove-context-provider/tests/typescript-with-provider-2/expected.tsx deleted file mode 100644 index 357cc7c..0000000 --- a/codemods/remove-context-provider/tests/typescript-with-provider-2/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -function App({ url }: { url: string }) { - const [theme, setTheme] = useState<'light' | 'dark'>('light'); - - return ( - - - - ); -} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-with-provider-2/metrics.json b/codemods/remove-context-provider/tests/typescript-with-provider-2/metrics.json deleted file mode 100644 index d7e29d4..0000000 --- a/codemods/remove-context-provider/tests/typescript-with-provider-2/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-context-provider-replacements": [ - { - "cardinality": { - "file": "tests/typescript-with-provider-2/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/typescript-with-provider/metrics.json b/codemods/remove-context-provider/tests/typescript-with-provider/metrics.json deleted file mode 100644 index 8bcb233..0000000 --- a/codemods/remove-context-provider/tests/typescript-with-provider/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-context-provider-replacements": [ - { - "cardinality": { - "file": "tests/typescript-with-provider/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/with-provider-2/expected.tsx b/codemods/remove-context-provider/tests/with-provider-2/expected.tsx deleted file mode 100644 index 9ef6573..0000000 --- a/codemods/remove-context-provider/tests/with-provider-2/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -function App() { - const [theme, setTheme] = useState('light'); - - return ( - - - - ); -} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/with-provider-2/metrics.json b/codemods/remove-context-provider/tests/with-provider-2/metrics.json deleted file mode 100644 index d0d0389..0000000 --- a/codemods/remove-context-provider/tests/with-provider-2/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-context-provider-replacements": [ - { - "cardinality": { - "file": "tests/with-provider-2/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-context-provider/tests/with-provider/metrics.json b/codemods/remove-context-provider/tests/with-provider/metrics.json deleted file mode 100644 index 10a3c05..0000000 --- a/codemods/remove-context-provider/tests/with-provider/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-context-provider-replacements": [ - { - "cardinality": { - "file": "tests/with-provider/input.tsx" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-context-provider/tsconfig.json b/codemods/remove-context-provider/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/remove-context-provider/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/remove-context-provider/workflow.yaml b/codemods/remove-context-provider/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/remove-context-provider/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/remove-forward-ref/README.md b/codemods/remove-forward-ref/README.md deleted file mode 100644 index 2ecdb85..0000000 --- a/codemods/remove-forward-ref/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/remove-forward-ref - -Inline `forwardRef(...)` render functions and pass `ref` through props. - -## Usage - -```bash -codemod run @react/remove-forward-ref -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/remove-forward-ref/codemod.yaml b/codemods/remove-forward-ref/codemod.yaml deleted file mode 100644 index f70874b..0000000 --- a/codemods/remove-forward-ref/codemod.yaml +++ /dev/null @@ -1,19 +0,0 @@ -schema_version: "1.0" - -name: "@react/remove-forward-ref" -version: "0.1.0" -description: "Inline forwardRef render functions and move ref into props" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React", "forwardRef"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/remove-forward-ref/package.json b/codemods/remove-forward-ref/package.json deleted file mode 100644 index 98a2bbe..0000000 --- a/codemods/remove-forward-ref/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@react/remove-forward-ref", - "version": "0.1.0", - "description": "Inline forwardRef render functions and move ref into props", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts --strictness loose", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "@types/node": "latest", - "typescript": "latest" - }, - "dependencies": { - "@jssg/utils": "^0.0.2" - } -} diff --git a/codemods/remove-forward-ref/scripts/codemod.ts b/codemods/remove-forward-ref/scripts/codemod.ts deleted file mode 100644 index 17fc65d..0000000 --- a/codemods/remove-forward-ref/scripts/codemod.ts +++ /dev/null @@ -1,279 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; -import { getImport, removeImport } from "@jssg/utils/javascript/imports"; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -function namedChildren(node: SgNode | null): SgNode[] { - if (!node) { - return []; - } - - return node.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function firstNamedChild(node: SgNode | null): SgNode | null { - return namedChildren(node)[0] ?? null; -} - -function callArguments(call: SgNode): SgNode[] { - return namedChildren(call.field("arguments")); -} - -function functionParameters(fn: SgNode): SgNode[] { - return namedChildren(fn.field("parameters")); -} - -function parameterPattern(param: SgNode): SgNode | null { - return firstNamedChild(param); -} - -function parameterTypeAnnotation(param: SgNode): SgNode | null { - const children = namedChildren(param); - return children.length > 1 ? children[1] ?? null : null; -} - -function legacyLiteralOrReferenceType(node: SgNode | null): SgNode | null { - const directNode = node && ( - node.kind() === "type_identifier" || - node.kind() === "nested_type_identifier" || - node.kind() === "object_type" || - node.kind() === "generic_type" - ) ? node : null; - const typeNode = directNode ?? ( - node && (node.kind() === "type_annotation" || node.kind() === "type_arguments") - ? firstNamedChild(node) - : null - ); - if (!typeNode) { - return null; - } - - return ( - typeNode.kind() === "type_identifier" || - typeNode.kind() === "nested_type_identifier" || - typeNode.kind() === "object_type" || - typeNode.kind() === "generic_type" - ) ? typeNode : null; -} - -function legacyReferenceType(node: SgNode | null): SgNode | null { - const directNode = node && ( - node.kind() === "type_identifier" || - node.kind() === "nested_type_identifier" || - node.kind() === "generic_type" - ) ? node : null; - const typeNode = directNode ?? ( - node && (node.kind() === "type_annotation" || node.kind() === "type_arguments") - ? firstNamedChild(node) - : null - ); - if (!typeNode) { - return null; - } - - return ( - typeNode.kind() === "type_identifier" || - typeNode.kind() === "nested_type_identifier" || - typeNode.kind() === "generic_type" - ) ? typeNode : null; -} - -function stripLeadingColon(text: string): string { - return text.startsWith(":") ? text.slice(1).trim() : text.trim(); -} - -function callTypeArguments(call: SgNode): SgNode[] { - const typeArguments = call.children().find((child) => child.kind() === "type_arguments"); - return namedChildren(typeArguments ?? null); -} - -function refTypeFromForwardedRef(typeAnnotation: SgNode | null): string | null { - const typeNode = legacyReferenceType(typeAnnotation); - if (!typeNode || typeNode.kind() !== "generic_type") { - return null; - } - - const children = namedChildren(typeNode); - const typeNameNode = children[0]; - const genericArgs = children[1]; - if (!typeNameNode || !genericArgs) { - return null; - } - - const typeNameChildren = namedChildren(typeNameNode); - const lastTypeName = typeNameChildren[typeNameChildren.length - 1] ?? typeNameNode; - if (lastTypeName.text() !== "ForwardedRef") { - return null; - } - - const refType = legacyReferenceType(genericArgs); - return refType?.text() ?? null; -} - -function refBindingEntry(refName: string): string { - return refName === "ref" ? "ref" : `ref: ${refName}`; -} - -function objectPatternInnerText(pattern: SgNode): string { - const text = pattern.text().trim(); - return text.startsWith("{") && text.endsWith("}") ? text.slice(1, -1).trim() : text; -} - -function propsPatternText(propsParam: SgNode, refName: string): string | null { - const pattern = parameterPattern(propsParam); - if (!pattern) { - return null; - } - - if (pattern.kind() === "identifier") { - return `{ ${refBindingEntry(refName)}, ...${pattern.text()} }`; - } - - if (pattern.kind() === "object_pattern") { - const inner = objectPatternInnerText(pattern); - return inner.length > 0 - ? `{ ${refBindingEntry(refName)}, ${inner} }` - : `{ ${refBindingEntry(refName)} }`; - } - - return null; -} - -function paramTypeText(propsParam: SgNode, call: SgNode, refParam: SgNode): string | null { - const typeArgs = callTypeArguments(call); - const refTypeArg = typeArgs[0] ?? null; - const propTypeArg = typeArgs[1] ?? null; - const legacyRefTypeArg = legacyReferenceType(refTypeArg); - const legacyPropTypeArg = legacyLiteralOrReferenceType(propTypeArg); - if (legacyRefTypeArg && legacyPropTypeArg) { - return `${legacyPropTypeArg.text()} & { ref: React.RefObject<${legacyRefTypeArg.text()}> }`; - } - - const propsType = legacyLiteralOrReferenceType(parameterTypeAnnotation(propsParam)); - if (!propsType) { - return null; - } - - const refType = refTypeFromForwardedRef(parameterTypeAnnotation(refParam)) ?? "unknown"; - return `${propsType.text()} & { ref: React.RefObject<${refType}> }`; -} - -function bodyText(fn: SgNode): string | null { - return fn.field("body")?.text() ?? null; -} - -function functionName(fn: SgNode): string { - return fn.children().find((child) => child.kind() === "identifier")?.text() ?? ""; -} - -function buildReplacement(call: SgNode): string | null { - const renderFunction = callArguments(call)[0]; - if (!renderFunction || (renderFunction.kind() !== "function_expression" && renderFunction.kind() !== "arrow_function")) { - return null; - } - - const params = functionParameters(renderFunction); - const propsParam = params[0]; - const refParam = params[1]; - if (!propsParam || !refParam) { - return null; - } - - const refPattern = parameterPattern(refParam); - if (!refPattern || refPattern.kind() !== "identifier") { - return null; - } - - const propsText = propsPatternText(propsParam, refPattern.text()); - const fnBodyText = bodyText(renderFunction); - if (!propsText || !fnBodyText) { - return null; - } - - const annotation = paramTypeText(propsParam, call, refParam); - const finalParam = annotation ? `${propsText}: ${annotation}` : propsText; - - if (renderFunction.kind() === "function_expression") { - const name = functionName(renderFunction); - const namePart = name.length > 0 ? ` ${name}` : ""; - return `function${namePart}(${finalParam}) ${fnBodyText}`; - } - - return `(${finalParam}) => ${fnBodyText}`; -} - -function isForwardRefIdentifierCall(call: SgNode, localName: string | null): boolean { - if (!localName) { - return false; - } - - const callee = call.field("function"); - return callee?.kind() === "identifier" && callee.text() === localName; -} - -function isForwardRefMemberCall(call: SgNode, reactLocalName: string | null): boolean { - if (!reactLocalName) { - return false; - } - - const callee = call.field("function"); - if (!callee || callee.kind() !== "member_expression") { - return false; - } - - const object = callee.field("object"); - const property = callee.field("property"); - return object?.kind() === "identifier" && - object.text() === reactLocalName && - property?.text() === "forwardRef"; -} - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const edits: Edit[] = []; - const metric = useMetricAtom("remove-forward-ref-replacements"); - - const forwardRefImport = getImport(rootNode, { type: "named", name: "forwardRef", from: "react" }); - const reactImport = getImport(rootNode, { type: "default", from: "react" }); - let replacements = 0; - - for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - if (!isForwardRefIdentifierCall(call, forwardRefImport?.alias ?? null) && - !isForwardRefMemberCall(call, reactImport?.alias ?? null)) { - continue; - } - - const replacement = buildReplacement(call); - if (!replacement) { - continue; - } - - edits.push(call.replace(replacement)); - replacements++; - } - - if (replacements === 0) { - return null; - } - - if (forwardRefImport) { - const removeEdit = removeImport(rootNode, { - type: "named", - specifiers: ["forwardRef"], - from: "react", - }); - if (removeEdit) { - edits.push(removeEdit); - } - } - - metric.increment({ file: metricFile(root.filename()) }, replacements); - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/remove-forward-ref/tests/arrow-function-expression/metrics.json b/codemods/remove-forward-ref/tests/arrow-function-expression/metrics.json deleted file mode 100644 index 0c57c24..0000000 --- a/codemods/remove-forward-ref/tests/arrow-function-expression/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/arrow-function-expression/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/callee-is-member-expression/metrics.json b/codemods/remove-forward-ref/tests/callee-is-member-expression/metrics.json deleted file mode 100644 index 4f8189b..0000000 --- a/codemods/remove-forward-ref/tests/callee-is-member-expression/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/callee-is-member-expression/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/forward-ref-import-2/metrics.json b/codemods/remove-forward-ref/tests/forward-ref-import-2/metrics.json deleted file mode 100644 index fede801..0000000 --- a/codemods/remove-forward-ref/tests/forward-ref-import-2/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/forward-ref-import-2/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/forward-ref-import/metrics.json b/codemods/remove-forward-ref/tests/forward-ref-import/metrics.json deleted file mode 100644 index 24e45e5..0000000 --- a/codemods/remove-forward-ref/tests/forward-ref-import/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/forward-ref-import/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/function-expression/metrics.json b/codemods/remove-forward-ref/tests/function-expression/metrics.json deleted file mode 100644 index 8e42ed8..0000000 --- a/codemods/remove-forward-ref/tests/function-expression/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/function-expression/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/expected.tsx b/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/expected.tsx deleted file mode 100644 index dc5bb49..0000000 --- a/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/expected.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { forwardRef } from 'react'; - -const MyComponent = forwardRef(function Component([props], ref) { - return null; -}); diff --git a/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/input.tsx b/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/input.tsx deleted file mode 100644 index dc5bb49..0000000 --- a/codemods/remove-forward-ref/tests/invalid-props-pattern-no-change/input.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { forwardRef } from 'react'; - -const MyComponent = forwardRef(function Component([props], ref) { - return null; -}); diff --git a/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/expected.tsx b/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/expected.tsx deleted file mode 100644 index a4303f4..0000000 --- a/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/expected.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { forwardRef } from 'react'; - -const MyComponent = forwardRef(null); diff --git a/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/input.tsx b/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/input.tsx deleted file mode 100644 index a4303f4..0000000 --- a/codemods/remove-forward-ref/tests/invalid-render-arg-no-change/input.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { forwardRef } from 'react'; - -const MyComponent = forwardRef(null); diff --git a/codemods/remove-forward-ref/tests/multiple-react-imports/expected.tsx b/codemods/remove-forward-ref/tests/multiple-react-imports/expected.tsx deleted file mode 100644 index 479c98b..0000000 --- a/codemods/remove-forward-ref/tests/multiple-react-imports/expected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { useState } from 'react'; - -const MyComponent = ( - { - ref, - ...props - } -) => { - return null; -}; diff --git a/codemods/remove-forward-ref/tests/multiple-react-imports/input.tsx b/codemods/remove-forward-ref/tests/multiple-react-imports/input.tsx deleted file mode 100644 index 5b6e9be..0000000 --- a/codemods/remove-forward-ref/tests/multiple-react-imports/input.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { useState } from 'react'; -import { forwardRef } from 'react'; - -const MyComponent = forwardRef((props, ref) => { - return null; -}); diff --git a/codemods/remove-forward-ref/tests/multiple-react-imports/metrics.json b/codemods/remove-forward-ref/tests/multiple-react-imports/metrics.json deleted file mode 100644 index eb74e11..0000000 --- a/codemods/remove-forward-ref/tests/multiple-react-imports/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/multiple-react-imports/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/props-identifier/metrics.json b/codemods/remove-forward-ref/tests/props-identifier/metrics.json deleted file mode 100644 index 31971f0..0000000 --- a/codemods/remove-forward-ref/tests/props-identifier/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/props-identifier/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/props-object-pattern/metrics.json b/codemods/remove-forward-ref/tests/props-object-pattern/metrics.json deleted file mode 100644 index c94ce24..0000000 --- a/codemods/remove-forward-ref/tests/props-object-pattern/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/props-object-pattern/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-props-type-literal/metrics.json b/codemods/remove-forward-ref/tests/typescript-props-type-literal/metrics.json deleted file mode 100644 index 7d4d936..0000000 --- a/codemods/remove-forward-ref/tests/typescript-props-type-literal/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/typescript-props-type-literal/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-qualified-names/expected.tsx b/codemods/remove-forward-ref/tests/typescript-qualified-names/expected.tsx deleted file mode 100644 index 5c8103e..0000000 --- a/codemods/remove-forward-ref/tests/typescript-qualified-names/expected.tsx +++ /dev/null @@ -1,10 +0,0 @@ -const MyComponent = function Component( - { - ref: myRef, - ...myProps - }: NS.Props & { - ref: React.RefObject - } -) { - return null; -}; diff --git a/codemods/remove-forward-ref/tests/typescript-qualified-names/input.tsx b/codemods/remove-forward-ref/tests/typescript-qualified-names/input.tsx deleted file mode 100644 index 833d99c..0000000 --- a/codemods/remove-forward-ref/tests/typescript-qualified-names/input.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { forwardRef } from 'react'; - -const MyComponent = forwardRef(function Component( - myProps: NS.Props, - myRef: React.ForwardedRef -) { - return null; -}); diff --git a/codemods/remove-forward-ref/tests/typescript-qualified-names/metrics.json b/codemods/remove-forward-ref/tests/typescript-qualified-names/metrics.json deleted file mode 100644 index 228e615..0000000 --- a/codemods/remove-forward-ref/tests/typescript-qualified-names/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/typescript-qualified-names/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/metrics.json b/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/metrics.json deleted file mode 100644 index 11fa3f9..0000000 --- a/codemods/remove-forward-ref/tests/typescript-type-arguments-custom-names/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/typescript-type-arguments-custom-names/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/metrics.json b/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/metrics.json deleted file mode 100644 index 2e70083..0000000 --- a/codemods/remove-forward-ref/tests/typescript-type-arguments-type-literals/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/typescript-type-arguments-type-literals/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-type-arguments/metrics.json b/codemods/remove-forward-ref/tests/typescript-type-arguments/metrics.json deleted file mode 100644 index 2ce2d16..0000000 --- a/codemods/remove-forward-ref/tests/typescript-type-arguments/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/typescript-type-arguments/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/expected.tsx b/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/expected.tsx deleted file mode 100644 index 24c40ee..0000000 --- a/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -type A = { a: 1 }; -type B = { b: 1 }; - -const MyComponent = function Component( - { - ref: myRef, - ...myProps - } -) { - return null; -}; diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/input.tsx b/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/input.tsx deleted file mode 100644 index 2182401..0000000 --- a/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { forwardRef } from 'react'; - -type A = { a: 1 }; -type B = { b: 1 }; - -const MyComponent = forwardRef(function Component( - myProps: A | B, - myRef: React.ForwardedRef -) { - return null; -}); diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/metrics.json b/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/metrics.json deleted file mode 100644 index 5d14c6e..0000000 --- a/codemods/remove-forward-ref/tests/typescript-unsupported-props-union/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/typescript-unsupported-props-union/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/expected.tsx b/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/expected.tsx deleted file mode 100644 index 6710ae3..0000000 --- a/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -type A = { a: 1 }; -type B = { b: 1 }; - -const MyComponent = ( - { - ref, - ...props - } -) => { - return null; -}; diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/input.tsx b/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/input.tsx deleted file mode 100644 index f7d4852..0000000 --- a/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/input.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { forwardRef } from 'react'; - -type A = { a: 1 }; -type B = { b: 1 }; - -const MyComponent = forwardRef((props, ref) => { - return null; -}); diff --git a/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/metrics.json b/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/metrics.json deleted file mode 100644 index 772591e..0000000 --- a/codemods/remove-forward-ref/tests/typescript-unsupported-type-arguments-union/metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "remove-forward-ref-replacements": [ - { - "cardinality": { - "file": "tests/typescript-unsupported-type-arguments-union/input.tsx" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/remove-forward-ref/tsconfig.json b/codemods/remove-forward-ref/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/remove-forward-ref/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/remove-forward-ref/workflow.yaml b/codemods/remove-forward-ref/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/remove-forward-ref/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/rename-unsafe-lifecycles/README.md b/codemods/rename-unsafe-lifecycles/README.md deleted file mode 100644 index 4ccaec6..0000000 --- a/codemods/rename-unsafe-lifecycles/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/rename-unsafe-lifecycles - -Rename deprecated React lifecycle names to their `UNSAFE_` equivalents in class components, object literals, and member references. - -## Usage - -```bash -codemod run @react/rename-unsafe-lifecycles -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/rename-unsafe-lifecycles/codemod.yaml b/codemods/rename-unsafe-lifecycles/codemod.yaml deleted file mode 100644 index 07f1e6f..0000000 --- a/codemods/rename-unsafe-lifecycles/codemod.yaml +++ /dev/null @@ -1,20 +0,0 @@ -schema_version: "1.0" - -name: "@react/rename-unsafe-lifecycles" -version: "0.1.0" -description: "Rename deprecated lifecycle methods to UNSAFE_ prefixed versions" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - - -targets: - languages: ["tsx"] - -keywords: ["React"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/rename-unsafe-lifecycles/package.json b/codemods/rename-unsafe-lifecycles/package.json deleted file mode 100644 index a8fed9d..0000000 --- a/codemods/rename-unsafe-lifecycles/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@react/rename-unsafe-lifecycles", - "version": "0.1.0", - "description": "Rename deprecated lifecycle methods to UNSAFE_ prefixed versions", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "typescript": "latest", - "@types/node": "latest" - } -} diff --git a/codemods/rename-unsafe-lifecycles/pnpm-lock.yaml b/codemods/rename-unsafe-lifecycles/pnpm-lock.yaml deleted file mode 100644 index 8dfa743..0000000 --- a/codemods/rename-unsafe-lifecycles/pnpm-lock.yaml +++ /dev/null @@ -1,32 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.0 - typescript: - specifier: latest - version: 5.9.3 - -packages: - - '@codemod.com/jssg-types@1.5.0': - resolution: {integrity: sha512-zChRbxI3hBSGrAHnWlEzOw1FztLWMMiarwcr0Wbk0On4hmv7dVgoUqpIHfxb64mEMKJ5syTIKY3ZNd8DcFQa5w==} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - -snapshots: - - '@codemod.com/jssg-types@1.5.0': {} - - typescript@5.9.3: {} diff --git a/codemods/rename-unsafe-lifecycles/scripts/codemod.ts b/codemods/rename-unsafe-lifecycles/scripts/codemod.ts deleted file mode 100644 index d65d934..0000000 --- a/codemods/rename-unsafe-lifecycles/scripts/codemod.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { Transform, Edit } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - - -const DEPRECATED_LIFECYCLES = { - componentWillMount: "UNSAFE_componentWillMount", - componentWillReceiveProps: "UNSAFE_componentWillReceiveProps", - componentWillUpdate: "UNSAFE_componentWillUpdate", -} as const; - -const transform: Transform = async (root) => { - const rootNode = root.root(); - const edits: Edit[] = []; - - - const transformMetric = useMetricAtom("lifecycle-renames"); - const renameCount: Record = {}; - - for (const [oldName, newName] of Object.entries(DEPRECATED_LIFECYCLES)) { - const methodDefs = rootNode.findAll({ - rule: { - any: [ - { - kind: "method_definition", - has: { - field: "name", - kind: "property_identifier", - regex: `^${oldName}$`, - }, - }, - { - kind: "public_field_definition", - has: { - field: "name", - kind: "property_identifier", - regex: `^${oldName}$`, - }, - }, - ], - }, - }); - - for (const method of methodDefs) { - const nameNode = method.find({ - rule: { - kind: "property_identifier", - regex: `^${oldName}$`, - }, - }); - - if (nameNode) { - edits.push(nameNode.replace(newName)); - renameCount[oldName] = (renameCount[oldName] || 0) + 1; - } - } - - const objectProps = rootNode.findAll({ - rule: { - kind: "pair", - has: { - field: "key", - kind: "property_identifier", - regex: `^${oldName}$`, - }, - }, - }); - - for (const prop of objectProps) { - const keyNode = prop.find({ - rule: { - kind: "property_identifier", - regex: `^${oldName}$`, - }, - }); - - if (keyNode) { - edits.push(keyNode.replace(newName)); - renameCount[oldName] = (renameCount[oldName] || 0) + 1; - } - } - - const memberExprs = rootNode.findAll({ - rule: { - kind: "member_expression", - has: { - field: "property", - kind: "property_identifier", - regex: `^${oldName}$`, - }, - }, - }); - - for (const memberExpr of memberExprs) { - const propertyNode = memberExpr.find({ - rule: { - kind: "property_identifier", - regex: `^${oldName}$`, - }, - }); - - if (propertyNode) { - edits.push(propertyNode.replace(newName)); - renameCount[oldName] = (renameCount[oldName] || 0) + 1; - } - } - } - - if (Object.keys(renameCount).length > 0) { - const lifecycles = Object.keys(renameCount).sort().join(","); - const total = Object.values(renameCount).reduce((a, b) => a + b, 0); - transformMetric.increment({ - lifecycles, - file: metricFile(root.filename()), - }, total); - } - - if (edits.length === 0) { - return null; - } - - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/rename-unsafe-lifecycles/tests/arrow-functions/metrics.json b/codemods/rename-unsafe-lifecycles/tests/arrow-functions/metrics.json deleted file mode 100644 index 56a8030..0000000 --- a/codemods/rename-unsafe-lifecycles/tests/arrow-functions/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "lifecycle-renames": [ - { - "cardinality": { - "file": "tests/arrow-functions/input.tsx", - "lifecycles": "componentWillMount,componentWillReceiveProps,componentWillUpdate" - }, - "count": 3 - } - ] -} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/create-react-class/metrics.json b/codemods/rename-unsafe-lifecycles/tests/create-react-class/metrics.json deleted file mode 100644 index 68c0180..0000000 --- a/codemods/rename-unsafe-lifecycles/tests/create-react-class/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "lifecycle-renames": [ - { - "cardinality": { - "file": "tests/create-react-class/input.tsx", - "lifecycles": "componentWillMount,componentWillReceiveProps,componentWillUpdate" - }, - "count": 6 - } - ] -} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/instance-methods/metrics.json b/codemods/rename-unsafe-lifecycles/tests/instance-methods/metrics.json deleted file mode 100644 index d59cca0..0000000 --- a/codemods/rename-unsafe-lifecycles/tests/instance-methods/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "lifecycle-renames": [ - { - "cardinality": { - "file": "tests/instance-methods/input.tsx", - "lifecycles": "componentWillMount,componentWillReceiveProps,componentWillUpdate" - }, - "count": 3 - } - ] -} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/metrics.json b/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/metrics.json deleted file mode 100644 index a91256f..0000000 --- a/codemods/rename-unsafe-lifecycles/tests/manually-calling-lifecycles/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "lifecycle-renames": [ - { - "cardinality": { - "file": "tests/manually-calling-lifecycles/input.tsx", - "lifecycles": "componentWillMount" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/metrics.json b/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/metrics.json deleted file mode 100644 index d83605b..0000000 --- a/codemods/rename-unsafe-lifecycles/tests/manually-invoked-mixin-methods/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "lifecycle-renames": [ - { - "cardinality": { - "file": "tests/manually-invoked-mixin-methods/input.tsx", - "lifecycles": "componentWillMount" - }, - "count": 2 - } - ] -} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/metrics.json b/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/metrics.json deleted file mode 100644 index 7997937..0000000 --- a/codemods/rename-unsafe-lifecycles/tests/one-lifecycle-calls-another/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "lifecycle-renames": [ - { - "cardinality": { - "file": "tests/one-lifecycle-calls-another/input.tsx", - "lifecycles": "componentWillMount,componentWillReceiveProps" - }, - "count": 3 - } - ] -} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/standalone-function/input.tsx b/codemods/rename-unsafe-lifecycles/tests/standalone-function/input.tsx deleted file mode 100644 index c515ccc..0000000 --- a/codemods/rename-unsafe-lifecycles/tests/standalone-function/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -function componentWillMount() { - // componentWillMount -} -function componentWillUpdate(nextProps, nextState) { - // componentWillUpdate -} -function componentWillReceiveProps(nextProps) { - // componentWillReceiveProps -} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/metrics.json b/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/metrics.json deleted file mode 100644 index 4119909..0000000 --- a/codemods/rename-unsafe-lifecycles/tests/typescript-class-tsx/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "lifecycle-renames": [ - { - "cardinality": { - "file": "tests/typescript-class-tsx/input.tsx", - "lifecycles": "componentWillMount,componentWillReceiveProps,componentWillUpdate" - }, - "count": 3 - } - ] -} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/input.tsx b/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/input.tsx deleted file mode 100644 index eb8979a..0000000 --- a/codemods/rename-unsafe-lifecycles/tests/variable-within-class-method/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -class SomeClass { - someMethod() { - const componentWillMount = true; - let componentWillUpdate = 123; - var componentWillReceiveProps = 'abc'; - } -} \ No newline at end of file diff --git a/codemods/rename-unsafe-lifecycles/tsconfig.json b/codemods/rename-unsafe-lifecycles/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/rename-unsafe-lifecycles/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/rename-unsafe-lifecycles/workflow.yaml b/codemods/rename-unsafe-lifecycles/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/rename-unsafe-lifecycles/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/replace-act-import/tsconfig.json b/codemods/replace-act-import/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/replace-act-import/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/replace-reactdom-render/README.md b/codemods/replace-reactdom-render/README.md deleted file mode 100644 index c545d33..0000000 --- a/codemods/replace-reactdom-render/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/replace-reactdom-render - -Replace `ReactDOM.render` and named `render` imports with `createRoot(...).render(...)`. - -## Usage - -```bash -codemod run @react/replace-reactdom-render -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/replace-reactdom-render/tsconfig.json b/codemods/replace-reactdom-render/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/replace-reactdom-render/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/replace-reactdom-render/workflow.yaml b/codemods/replace-reactdom-render/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/replace-reactdom-render/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/replace-string-ref/tsconfig.json b/codemods/replace-string-ref/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/replace-string-ref/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/replace-string-ref/workflow.yaml b/codemods/replace-string-ref/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/replace-string-ref/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/replace-use-form-state/README.md b/codemods/replace-use-form-state/README.md deleted file mode 100644 index 9b9ec51..0000000 --- a/codemods/replace-use-form-state/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/replace-use-form-state - -Rename `useFormState` to `useActionState` in `react-dom` imports and usages. - -## Usage - -```bash -codemod run @react/replace-use-form-state -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/replace-use-form-state/tsconfig.json b/codemods/replace-use-form-state/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/replace-use-form-state/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/sort-comp/README.md b/codemods/sort-comp/README.md deleted file mode 100644 index 0b73b11..0000000 --- a/codemods/sort-comp/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# @react/sort-comp - -Reorder React component members to match the `react/sort-comp` rule across `createClass` components and class components. - -## Usage - -```bash -codemod run @react/sort-comp -``` - -## Options - -- `methodsOrder`: ordered list of method groups and names used for sorting. Defaults to the built-in React sort order used by this package. -- `explicit-require`: when `false`, run even if no React import/require is present. Default: `true`. - -## Development - -```bash -pnpm test -pnpm check-types -``` - -The remaining blocker is full parity for shareable / fully resolved ESLint config resolution. The legacy codemod delegated that to ESLint’s full config machinery. Inside JSSG, I implemented a clean classic-config reader that covers the legacy fixture cases and direct local config branches, but I did not ship a heavier config-execution workaround for every ESLint extends shape. diff --git a/codemods/sort-comp/codemod.yaml b/codemods/sort-comp/codemod.yaml deleted file mode 100644 index 04eb095..0000000 --- a/codemods/sort-comp/codemod.yaml +++ /dev/null @@ -1,20 +0,0 @@ -schema_version: "1.0" - -name: "@react/sort-comp" -version: "0.1.0" -description: "Reorder React component methods to match eslint-plugin-react sort-comp rule" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - -targets: - languages: ["tsx"] - -keywords: ["React"] - -registry: - access: "public" - visibility: "public" - -capabilities: - - fs diff --git a/codemods/sort-comp/package.json b/codemods/sort-comp/package.json deleted file mode 100644 index b1a4acf..0000000 --- a/codemods/sort-comp/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@react/sort-comp", - "version": "0.1.0", - "description": "Reorder React component methods to match eslint-plugin-react sort-comp rule", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "@types/node": "latest", - "typescript": "latest" - } -} diff --git a/codemods/sort-comp/scripts/codemod.ts b/codemods/sort-comp/scripts/codemod.ts deleted file mode 100644 index 0ecab8a..0000000 --- a/codemods/sort-comp/scripts/codemod.ts +++ /dev/null @@ -1,626 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; -import * as fs from "fs"; -import path from "path"; -import { pathToFileURL } from "url"; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -type ReactImportAliases = { - component: Set; - pureComponent: Set; -}; - -type ReactSuperclassKind = "Component" | "PureComponent"; - -const REACT_SOURCES = new Set(["react", "React", "react/addons", "react-native"]); - -function stringValue(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - return text.length >= 2 ? text.slice(1, -1) : null; -} - -function callArguments(call: SgNode): SgNode[] { - const args = call.field("arguments"); - if (!args) return []; - return args.children().filter((child) => child.isNamed() && child.kind() !== "comment"); -} - -function requireSource(call: SgNode): string | null { - const callee = call.field("function"); - if (!callee || callee.kind() !== "identifier" || callee.text() !== "require") return null; - const firstArg = callArguments(call)[0]; - return firstArg?.kind() === "string" ? stringValue(firstArg) : null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? stringValue(source) : null; -} - -function isReactSource(importStmt: SgNode): boolean { - return REACT_SOURCES.has(importSource(importStmt) ?? ""); -} - -function hasReact(rootNode: SgNode): boolean { - for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (isReactSource(importNode)) return true; - } - - for (const call of rootNode.findAll({ rule: { kind: "call_expression" } })) { - if (REACT_SOURCES.has(requireSource(call) ?? "")) return true; - } - - return false; -} - -function explicitRequireDisabled(value: unknown): boolean { - return value === false || value === "false"; -} - -function reactImportAliases(rootNode: SgNode): ReactImportAliases { - const component = new Set(); - const pureComponent = new Set(); - - for (const importStmt of rootNode.findAll({ rule: { kind: "import_statement" } })) { - if (!isReactSource(importStmt)) continue; - - for (const specifier of importStmt.findAll({ rule: { kind: "import_specifier" } })) { - const imported = specifier.field("name")?.text(); - const alias = specifier.field("alias")?.text() ?? imported; - if (!imported || !alias) continue; - if (imported === "Component") component.add(alias); - if (imported === "PureComponent") pureComponent.add(alias); - } - } - - return { component, pureComponent }; -} - -function reactSuperclassKind( - classDecl: SgNode, - aliases: ReactImportAliases, -): ReactSuperclassKind | null { - const heritage = classDecl.find({ rule: { kind: "class_heritage" } }); - if (!heritage) return null; - - const directExtends = heritage.find({ - rule: { any: [{ kind: "member_expression" }, { kind: "identifier" }] }, - }); - if (!directExtends) return null; - - if (directExtends.kind() === "member_expression") { - const object = directExtends.field("object"); - const property = directExtends.field("property")?.text() ?? ""; - if (object?.text() !== "React") return null; - if (property === "Component" || property === "PureComponent") return property; - return null; - } - - const name = directExtends.text(); - if (aliases.component.has(name)) return "Component"; - if (aliases.pureComponent.has(name)) return "PureComponent"; - return null; -} - -function isReactCreateClassCall(node: SgNode | null): node is SgNode { - if (!node || node.kind() !== "call_expression") return false; - const callee = node.field("function"); - return callee?.kind() === "member_expression" && - callee.field("object")?.kind() === "identifier" && - callee.field("object")?.text() === "React" && - callee.field("property")?.kind() === "property_identifier" && - callee.field("property")?.text() === "createClass"; -} - -function reactCreateClassConfigs(rootNode: SgNode): SgNode[] { - const configs: SgNode[] = []; - for (const declarator of rootNode.findAll({ rule: { kind: "variable_declarator" } })) { - const value = declarator.field("value"); - if (!isReactCreateClassCall(value)) continue; - const args = value.field("arguments"); - const config = args?.children().find((child) => child.kind() === "object"); - if (config) configs.push(config); - } - return configs; -} - -function reactClassBodies(rootNode: SgNode): SgNode[] { - const aliases = reactImportAliases(rootNode); - const reactClasses = rootNode.findAll({ rule: { kind: "class_declaration" } }) - .map((classDecl) => ({ classDecl, kind: reactSuperclassKind(classDecl, aliases) })) - .filter((entry): entry is { classDecl: SgNode; kind: ReactSuperclassKind } => entry.kind !== null); - const targetSuperclassKind = reactClasses.some((entry) => entry.kind === "Component") - ? "Component" - : reactClasses.some((entry) => entry.kind === "PureComponent") - ? "PureComponent" - : null; - - if (!targetSuperclassKind) return []; - return reactClasses - .filter((entry) => entry.kind === targetSuperclassKind) - .map((entry) => entry.classDecl.find({ rule: { kind: "class_body" } })) - .filter((body): body is SgNode => body !== null); -} - - -type MemberChunk = { - node: SgNode; - name: string; - text: string; - start: number; - end: number; - originalIndex: number; -}; - -const DEFAULT_METHODS_ORDER = [ - "static-methods", - "displayName", - "propTypes", - "contextTypes", - "childContextTypes", - "mixins", - "statics", - "defaultProps", - "constructor", - "getDefaultProps", - "state", - "getInitialState", - "getChildContext", - "getDerivedStateFromProps", - "componentWillMount", - "UNSAFE_componentWillMount", - "componentDidMount", - "componentWillReceiveProps", - "UNSAFE_componentWillReceiveProps", - "shouldComponentUpdate", - "componentWillUpdate", - "UNSAFE_componentWillUpdate", - "getSnapshotBeforeUpdate", - "componentDidUpdate", - "componentDidCatch", - "componentWillUnmount", - "/^on.+$/", - "/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/", - "everything-else", - "/^render.+$/", - "render", -]; - -type SortCompRuleConfig = { - order?: string[]; - groups?: Record; -}; - -const ESLINT_CONFIG_FILES = [ - ".eslintrc", - ".eslintrc.json", - ".eslintrc.yaml", - ".eslintrc.yml", - ".eslintrc.js", - ".eslintrc.cjs", - ".eslintrc.mjs", - "package.json", -]; - -function isStringArray(value: unknown): value is string[] { - return Array.isArray(value) && value.every((entry) => typeof entry === "string"); -} - -function stringArrayParam(value: unknown): string[] | null { - if (isStringArray(value)) return value; - if (typeof value !== "string") return null; - try { - const parsed = JSON.parse(value) as unknown; - return isStringArray(parsed) ? parsed : null; - } catch { - return null; - } -} - -function isGroupsMap(value: unknown): value is Record { - return typeof value === "object" && - value !== null && - Object.values(value).every((entry) => isStringArray(entry)); -} - -function normalizeSortCompRuleConfig(value: unknown): SortCompRuleConfig | null { - if (typeof value !== "object" || value === null) return null; - const maybeConfig = value as { order?: unknown; groups?: unknown }; - const config: SortCompRuleConfig = {}; - if (maybeConfig.order !== undefined) { - if (!isStringArray(maybeConfig.order)) return null; - config.order = maybeConfig.order; - } - if (maybeConfig.groups !== undefined) { - if (!isGroupsMap(maybeConfig.groups)) return null; - config.groups = maybeConfig.groups; - } - return config; -} - -async function getMethodsOrderFromEslint(filePath: string): Promise { - let dir = path.dirname(filePath); - while (true) { - for (const configName of ESLINT_CONFIG_FILES) { - const configPath = path.join(dir, configName); - const methodsOrder = await methodsOrderFromConfigFile(configPath, new Set()); - if (methodsOrder) return methodsOrder; - } - const parent = path.dirname(dir); - if (parent === dir) return null; - dir = parent; - } -} - -function resolveMethodsOrder(ruleConfig: SortCompRuleConfig): string[] | null { - if (!ruleConfig.order) return null; - const groups = ruleConfig.groups ?? {}; - const resolvedOrder: string[] = []; - for (const entry of ruleConfig.order) { - const groupEntries = groups[entry]; - if (groupEntries) { - resolvedOrder.push(...groupEntries); - } else { - resolvedOrder.push(entry); - } - } - return resolvedOrder; -} - -function yamlRuleConfig(text: string): SortCompRuleConfig | null { - const lines = text.split("\n"); - const sortCompIndex = lines.findIndex((line) => line.trim() === "react/sort-comp:"); - if (sortCompIndex === -1) return null; - - const ruleConfig: SortCompRuleConfig = {}; - let mode: "order" | "groups" | "group-items" | null = null; - let currentGroup: string | null = null; - - for (let i = sortCompIndex + 1; i < lines.length; i++) { - const line = lines[i]!; - const trimmed = line.trim(); - if (trimmed === "") continue; - const indent = line.match(/^ */)?.[0].length ?? 0; - if (indent <= 2) break; - if (trimmed === "- 0" || trimmed === "- off" || trimmed === "- warn" || trimmed === "- error") continue; - - if (trimmed === "- order:" || trimmed === "order:") { - ruleConfig.order = []; - mode = "order"; - currentGroup = null; - continue; - } - - if (trimmed === "groups:" || trimmed === "- groups:") { - ruleConfig.groups = {}; - mode = "groups"; - currentGroup = null; - continue; - } - - if (mode === "order" && trimmed.startsWith("- ")) { - ruleConfig.order?.push(trimmed.slice(2)); - continue; - } - - if ((mode === "groups" || mode === "group-items") && trimmed.endsWith(":") && !trimmed.startsWith("- ")) { - currentGroup = trimmed.slice(0, -1); - ruleConfig.groups ??= {}; - ruleConfig.groups[currentGroup] = []; - mode = "group-items"; - continue; - } - - if (mode === "group-items" && currentGroup && trimmed.startsWith("- ")) { - ruleConfig.groups?.[currentGroup]?.push(trimmed.slice(2)); - continue; - } - } - - return ruleConfig.order ? ruleConfig : null; -} - -async function loadConfigFile(configPath: string): Promise { - const basename = path.basename(configPath); - if (basename === "package.json") { - try { - const parsed = JSON.parse(fs.readFileSync(configPath, "utf8")) as { eslintConfig?: unknown }; - return parsed.eslintConfig ?? null; - } catch { - return null; - } - } - - if (configPath.endsWith(".js") || configPath.endsWith(".cjs") || configPath.endsWith(".mjs")) { - try { - const imported = await import(pathToFileURL(configPath).href); - return ("default" in imported ? imported.default : imported) as unknown; - } catch { - return null; - } - } - - let text: string; - try { - text = fs.readFileSync(configPath, "utf8"); - } catch { - return null; - } - const trimmed = text.trimStart(); - if (trimmed.startsWith("{")) { - try { - return JSON.parse(text) as unknown; - } catch { - return null; - } - } - - return { rules: { "react/sort-comp": [0, yamlRuleConfig(text)] } }; -} - -function directRuleConfig(config: unknown): SortCompRuleConfig | null { - if (typeof config !== "object" || config === null) return null; - const maybeRules = (config as { rules?: unknown }).rules; - if (typeof maybeRules !== "object" || maybeRules === null) return null; - const sortCompRule = (maybeRules as Record)["react/sort-comp"]; - if (!Array.isArray(sortCompRule) || sortCompRule.length < 2) return null; - return normalizeSortCompRuleConfig(sortCompRule[1]); -} - -function normalizeExtends(value: unknown): string[] { - if (typeof value === "string") return [value]; - return isStringArray(value) ? value : []; -} - -function resolveRelativeExtend(baseDir: string, specifier: string): string | null { - if (!specifier.startsWith(".") && !path.isAbsolute(specifier)) return null; - const candidates = [ - specifier, - `${specifier}.js`, - `${specifier}.cjs`, - `${specifier}.mjs`, - `${specifier}.json`, - `${specifier}.yaml`, - `${specifier}.yml`, - ].map((candidate) => path.resolve(baseDir, candidate)); - for (const candidate of candidates) { - try { - fs.readFileSync(candidate, "utf8"); - return candidate; - } catch { - continue; - } - } - return null; -} - -function tryReadText(filePath: string): string | null { - try { - return fs.readFileSync(filePath, "utf8"); - } catch { - return null; - } -} - -function resolvePackageExtend(baseDir: string, specifier: string): string | null { - if (specifier.startsWith(".") || path.isAbsolute(specifier) || specifier.startsWith("eslint:") || specifier.startsWith("plugin:")) { - return null; - } - - const parts = specifier.split("/"); - const isScoped = specifier.startsWith("@"); - const packagePartLength = isScoped ? 2 : 1; - const packageName = parts.slice(0, packagePartLength).join("/"); - const subpath = parts.slice(packagePartLength).join("/"); - const packageCandidates = (() => { - if (isScoped) { - const [scope, name] = packageName.split("/"); - return name.startsWith("eslint-config-") - ? [packageName] - : [packageName, `${scope}/eslint-config-${name}`]; - } - return packageName.startsWith("eslint-config-") - ? [packageName] - : [packageName, `eslint-config-${packageName}`]; - })(); - - let dir = baseDir; - while (true) { - for (const packageCandidate of packageCandidates) { - const packageRoot = path.join(dir, "node_modules", packageCandidate); - const directCandidate = subpath - ? resolveRelativeExtend(packageRoot, subpath) - : null; - if (directCandidate) return directCandidate; - - const packageJsonPath = path.join(packageRoot, "package.json"); - const packageJsonText = tryReadText(packageJsonPath); - if (packageJsonText) { - try { - const pkg = JSON.parse(packageJsonText) as { main?: string }; - const mainField = typeof pkg.main === "string" ? pkg.main : "index.js"; - const mainCandidate = resolveRelativeExtend(packageRoot, mainField); - if (mainCandidate) return mainCandidate; - } catch { - // ignore malformed package json - } - } - - const indexCandidate = resolveRelativeExtend(packageRoot, "index"); - if (indexCandidate) return indexCandidate; - } - - const parent = path.dirname(dir); - if (parent === dir) return null; - dir = parent; - } -} - -async function methodsOrderFromConfigFile( - configPath: string, - seen: Set, -): Promise { - if (seen.has(configPath)) return null; - seen.add(configPath); - - const config = await loadConfigFile(configPath); - const direct = directRuleConfig(config); - const resolvedDirect = direct ? resolveMethodsOrder(direct) : null; - if (resolvedDirect) return resolvedDirect; - - if (typeof config !== "object" || config === null) return null; - const extendsEntries = normalizeExtends((config as { extends?: unknown }).extends); - for (const entry of extendsEntries) { - const resolvedPath = resolveRelativeExtend(path.dirname(configPath), entry) ?? - resolvePackageExtend(path.dirname(configPath), entry); - if (!resolvedPath) continue; - const resolvedOrder = await methodsOrderFromConfigFile(resolvedPath, seen); - if (resolvedOrder) return resolvedOrder; - } - - return null; -} - -function isTypeAnnotationMember(node: SgNode): boolean { - return node.kind() === "public_field_definition" && - node.field("value") === null && - node.find({ rule: { kind: "type_annotation" } }) !== null; -} - -function selectorMatches(selector: string, name: string, isStatic: boolean): boolean { - if (selector === "static-methods") return isStatic; - if (selector === "type-annotations") return false; - if (selector === name) return true; - if (selector.startsWith("/") && selector.lastIndexOf("/") > 0) { - const lastSlash = selector.lastIndexOf("/"); - const body = selector.slice(1, lastSlash); - const flags = selector.slice(lastSlash + 1); - try { - return new RegExp(body, flags).test(name); - } catch { - return false; - } - } - return false; -} - -function chunkMatchesSelector(selector: string, chunk: MemberChunk): boolean { - if (selector === "type-annotations") return isTypeAnnotationMember(chunk.node); - return selectorMatches(selector, chunk.name, chunk.node.text().startsWith("static ")); -} - -function correctIndex(order: string[], chunk: MemberChunk): number { - const everythingElse = order.indexOf("everything-else"); - for (let i = 0; i < order.length; i++) { - if (i !== everythingElse && chunkMatchesSelector(order[i]!, chunk)) return i; - } - return everythingElse >= 0 ? everythingElse : Number.POSITIVE_INFINITY; -} - -function memberName(node: SgNode): string { - const fieldName = node.field("name")?.text() ?? node.field("key")?.text(); - if (fieldName) return fieldName; - return node.children().find((child) => - child.kind() === "property_identifier" || child.kind() === "identifier" - )?.text() ?? ""; -} - -function significantChildren(container: SgNode): SgNode[] { - return container.children() - .filter((child) => - child.kind() === "comment" || - child.kind() === "pair" || - child.kind() === "method_definition" || - child.kind() === "public_field_definition" - ) - .sort((a, b) => a.range().start.index - b.range().start.index); -} - -function collectChunks(container: SgNode, source: string): MemberChunk[] { - const chunks: MemberChunk[] = []; - let leadingStart: number | null = null; - - for (const child of significantChildren(container)) { - if (child.kind() === "comment") { - if (leadingStart === null) leadingStart = child.range().start.index; - continue; - } - const start = leadingStart ?? child.range().start.index; - const end = child.range().end.index; - chunks.push({ - node: child, - name: memberName(child), - text: source.slice(start, end).trim(), - start, - end, - originalIndex: chunks.length, - }); - leadingStart = null; - } - - return chunks; -} - -function sortChunks(chunks: MemberChunk[], order: string[]): MemberChunk[] { - return [...chunks].sort((a, b) => { - const indexA = correctIndex(order, a); - const indexB = correctIndex(order, b); - if (indexA !== indexB) return indexA - indexB; - const nameCompare = a.name.localeCompare(b.name); - return nameCompare !== 0 ? nameCompare : a.originalIndex - b.originalIndex; - }); -} - -function replacementFor(container: SgNode, chunks: MemberChunk[], order: string[], sep: string): Edit | null { - if (chunks.length <= 1) return null; - const sorted = sortChunks(chunks, order); - const changed = sorted.some((chunk, index) => chunk.originalIndex !== index); - if (!changed) return null; - return { - startPos: chunks[0]!.start, - endPos: chunks[chunks.length - 1]!.end, - insertedText: sorted.map((chunk) => chunk.text).join(sep), - }; -} - -const transform: Transform = async (root, options) => { - const rootNode = root.root(); - if (!explicitRequireDisabled(options.params?.["explicit-require"]) && !hasReact(rootNode)) { - return null; - } - - const source = rootNode.text(); - const edits: Edit[] = []; - const metric = useMetricAtom("sort-comp-reorders"); - const methodsOrder = stringArrayParam(options.params?.methodsOrder) ?? - await getMethodsOrderFromEslint(root.filename()) ?? - DEFAULT_METHODS_ORDER; - - for (const config of reactCreateClassConfigs(rootNode)) { - const edit = replacementFor(config, collectChunks(config, source), methodsOrder, ",\n\n "); - if (edit) { - edits.push(edit); - metric.increment({ file: metricFile(root.filename()), kind: "createClass" }); - } - } - - for (const body of reactClassBodies(rootNode)) { - const edit = replacementFor(body, collectChunks(body, source), methodsOrder, "\n\n "); - if (edit) { - edits.push(edit); - metric.increment({ file: metricFile(root.filename()), kind: "class" }); - } - } - - if (edits.length === 0) return null; - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/sort-comp/tests/custom-sort-group/metrics.json b/codemods/sort-comp/tests/custom-sort-group/metrics.json deleted file mode 100644 index ee86a13..0000000 --- a/codemods/sort-comp/tests/custom-sort-group/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/custom-sort-group/input.tsx", - "kind": "createClass" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/sort-comp/tests/custom-sort/metrics.json b/codemods/sort-comp/tests/custom-sort/metrics.json deleted file mode 100644 index 96517a4..0000000 --- a/codemods/sort-comp/tests/custom-sort/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/custom-sort/input.tsx", - "kind": "createClass" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx b/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx deleted file mode 100644 index 7184b23..0000000 --- a/codemods/sort-comp/tests/identifier-create-class-no-change/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var createClass = require('create-react-class'); - -var C = createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx b/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx deleted file mode 100644 index 7184b23..0000000 --- a/codemods/sort-comp/tests/identifier-create-class-no-change/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -var createClass = require('create-react-class'); - -var C = createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/methods-order-option/expected.tsx b/codemods/sort-comp/tests/methods-order-option/expected.tsx deleted file mode 100644 index 658d96c..0000000 --- a/codemods/sort-comp/tests/methods-order-option/expected.tsx +++ /dev/null @@ -1,11 +0,0 @@ -var React = require('react/addons'); - -var C = React.createClass({ - componentDidMount() {}, - - z() {}, - - render() { - return
    ; - }, -}); diff --git a/codemods/sort-comp/tests/methods-order-option/input.tsx b/codemods/sort-comp/tests/methods-order-option/input.tsx deleted file mode 100644 index 81a901c..0000000 --- a/codemods/sort-comp/tests/methods-order-option/input.tsx +++ /dev/null @@ -1,11 +0,0 @@ -var React = require('react/addons'); - -var C = React.createClass({ - render() { - return
    ; - }, - - z() {}, - - componentDidMount() {}, -}); diff --git a/codemods/sort-comp/tests/methods-order-option/metrics.json b/codemods/sort-comp/tests/methods-order-option/metrics.json deleted file mode 100644 index 1723811..0000000 --- a/codemods/sort-comp/tests/methods-order-option/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/methods-order-option/input.tsx", - "kind": "createClass" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/sort-comp/tests/methods-order-option/test.config.json b/codemods/sort-comp/tests/methods-order-option/test.config.json deleted file mode 100644 index f70c53c..0000000 --- a/codemods/sort-comp/tests/methods-order-option/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "methodsOrder": ["componentDidMount", "everything-else", "render"] - } -} diff --git a/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx b/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx deleted file mode 100644 index 152ec11..0000000 --- a/codemods/sort-comp/tests/no-react-explicit-require/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var C = React.createClass({ - a() {}, - - render() { - return
    ; - }, -}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/input.tsx b/codemods/sort-comp/tests/no-react-explicit-require/input.tsx deleted file mode 100644 index d86a4e7..0000000 --- a/codemods/sort-comp/tests/no-react-explicit-require/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var C = React.createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/no-react-explicit-require/metrics.json b/codemods/sort-comp/tests/no-react-explicit-require/metrics.json deleted file mode 100644 index a24fb9d..0000000 --- a/codemods/sort-comp/tests/no-react-explicit-require/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/no-react-explicit-require/input.tsx", - "kind": "createClass" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/sort-comp/tests/no-react-explicit-require/test.config.json b/codemods/sort-comp/tests/no-react-explicit-require/test.config.json deleted file mode 100644 index 4858435..0000000 --- a/codemods/sort-comp/tests/no-react-explicit-require/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "explicit-require": false - } -} diff --git a/codemods/sort-comp/tests/no-react-no-change/expected.tsx b/codemods/sort-comp/tests/no-react-no-change/expected.tsx deleted file mode 100644 index d86a4e7..0000000 --- a/codemods/sort-comp/tests/no-react-no-change/expected.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var C = React.createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/no-react-no-change/input.tsx b/codemods/sort-comp/tests/no-react-no-change/input.tsx deleted file mode 100644 index d86a4e7..0000000 --- a/codemods/sort-comp/tests/no-react-no-change/input.tsx +++ /dev/null @@ -1,7 +0,0 @@ -var C = React.createClass({ - render() { - return
    ; - }, - - a() {}, -}); diff --git a/codemods/sort-comp/tests/sort-comp-pure/metrics.json b/codemods/sort-comp/tests/sort-comp-pure/metrics.json deleted file mode 100644 index b40335c..0000000 --- a/codemods/sort-comp/tests/sort-comp-pure/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/sort-comp-pure/input.tsx", - "kind": "class" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/sort-comp/tests/sort-comp/metrics.json b/codemods/sort-comp/tests/sort-comp/metrics.json deleted file mode 100644 index 9f8d4e0..0000000 --- a/codemods/sort-comp/tests/sort-comp/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/sort-comp/input.tsx", - "kind": "createClass" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/sort-comp/tests/sort-comp2/metrics.json b/codemods/sort-comp/tests/sort-comp2/metrics.json deleted file mode 100644 index 2d17b75..0000000 --- a/codemods/sort-comp/tests/sort-comp2/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/sort-comp2/input.tsx", - "kind": "class" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/sort-comp/tests/sort-comp3/metrics.json b/codemods/sort-comp/tests/sort-comp3/metrics.json deleted file mode 100644 index ea921ea..0000000 --- a/codemods/sort-comp/tests/sort-comp3/metrics.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "sort-comp-reorders": [ - { - "cardinality": { - "file": "tests/sort-comp3/input.tsx", - "kind": "class" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx b/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx deleted file mode 100644 index 97a64fb..0000000 --- a/codemods/sort-comp/tests/unrelated-component-no-change/expected.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from 'other-lib'; - -class C extends Component { - render() { - return
    ; - } - - a() {} -} diff --git a/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx b/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx deleted file mode 100644 index 97a64fb..0000000 --- a/codemods/sort-comp/tests/unrelated-component-no-change/input.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { Component } from 'other-lib'; - -class C extends Component { - render() { - return
    ; - } - - a() {} -} diff --git a/codemods/sort-comp/tsconfig.json b/codemods/sort-comp/tsconfig.json deleted file mode 100644 index ed30c5e..0000000 --- a/codemods/sort-comp/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/sort-comp/workflow.yaml b/codemods/sort-comp/workflow.yaml deleted file mode 100644 index a80a53d..0000000 --- a/codemods/sort-comp/workflow.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -params: - schema: - methodsOrder: - name: "Methods Order" - description: "Ordered list of method groups and member names used for sorting." - type: array - items: - type: string - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/update-react-imports/.gitignore b/codemods/update-react-imports/.gitignore deleted file mode 100644 index 78174f4..0000000 --- a/codemods/update-react-imports/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# Dependencies -node_modules/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Build artifacts -target/ -dist/ -build/ - -# Temporary files -*.tmp -*.temp -.cache/ - -# Environment files -.env -.env.local - -# IDE files -.vscode/ -.idea/ -*.swp -*.swo - -# OS files -.DS_Store -Thumbs.db - -# Package bundles -*.tar.gz -*.tgz \ No newline at end of file diff --git a/codemods/update-react-imports/README.md b/codemods/update-react-imports/README.md deleted file mode 100644 index ce21ed8..0000000 --- a/codemods/update-react-imports/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# @react/update-react-imports - -Remove unnecessary React imports, convert member access to named imports when safe, and keep namespace imports when `React` is used as a value or type namespace. - -## Usage - -```bash -codemod run @react/update-react-imports -``` - -## Options - -- `destructureNamespaceImports`: allow namespace imports such as `import * as React` to be destructured into named imports when it is safe to do so. - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/update-react-imports/codemod.yaml b/codemods/update-react-imports/codemod.yaml deleted file mode 100644 index a7eaeb1..0000000 --- a/codemods/update-react-imports/codemod.yaml +++ /dev/null @@ -1,20 +0,0 @@ -schema_version: "1.0" - -name: "@react/update-react-imports" -version: "0.1.0" -description: "Remove unused React default import and convert to named imports" -author: "Codemod " -license: "MIT" -workflow: "workflow.yaml" - - -targets: - languages: ["tsx"] - -keywords: ["React"] - -registry: - access: "public" - visibility: "public" - -capabilities: [] diff --git a/codemods/update-react-imports/package.json b/codemods/update-react-imports/package.json deleted file mode 100644 index 60da8b8..0000000 --- a/codemods/update-react-imports/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@react/update-react-imports", - "version": "0.1.0", - "description": "Remove unused React default import and convert to named imports", - "type": "module", - "scripts": { - "test": "pnpm dlx codemod@latest jssg test -l tsx ./scripts/codemod.ts", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@codemod.com/jssg-types": "latest", - "typescript": "latest", - "@types/node": "latest" - }, - "dependencies": { - "@jssg/utils": "^0.0.2" - } -} diff --git a/codemods/update-react-imports/scripts/codemod.ts b/codemods/update-react-imports/scripts/codemod.ts deleted file mode 100644 index d1e58da..0000000 --- a/codemods/update-react-imports/scripts/codemod.ts +++ /dev/null @@ -1,250 +0,0 @@ -import type { Transform, Edit, SgNode } from "codemod:ast-grep"; -import type TSX from "codemod:ast-grep/langs/tsx"; -import { useMetricAtom } from "codemod:metrics"; - -function metricFile(filename: string): string { - const cwd = process.cwd() + "/"; - return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; -} - -type Specifier = { name: string; alias?: string; isType: boolean }; - -function isWhitespace(char: string | undefined): boolean { - return char === " " || char === "\t" || char === "\n" || char === "\r"; -} - -function sourceText(node: SgNode): string | null { - const fragment = node.find({ rule: { kind: "string_fragment" } }); - if (fragment) return fragment.text(); - const text = node.text(); - if (text.length >= 2) return text.slice(1, -1); - return null; -} - -function importSource(node: SgNode): string | null { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - return source ? sourceText(source) : null; -} - -function isReactImport(node: SgNode): boolean { - const source = importSource(node); - return source === "react" || source === "React"; -} - -function statementRange(node: SgNode, source: string): { start: number; end: number } { - const range = node.range(); - let end = range.end.index; - while (end < source.length && (source[end] === "\n" || source[end] === "\r")) end++; - return { start: range.start.index, end }; -} - -function formatSpec(spec: Specifier): string { - const name = spec.alias && spec.alias !== spec.name ? `${spec.name} as ${spec.alias}` : spec.name; - return spec.isType ? `type ${name}` : name; -} - -function uniqueSpecs(specs: Specifier[]): Specifier[] { - const seen = new Set(); - const result: Specifier[] = []; - for (const spec of specs) { - const key = `${spec.isType}:${spec.name}:${spec.alias ?? ""}`; - if (!seen.has(key)) { - seen.add(key); - result.push(spec); - } - } - return result; -} - -function splitSpecs(specs: Specifier[]): { regular: Specifier[]; type: Specifier[] } { - return { - regular: uniqueSpecs(specs.filter((s) => !s.isType)), - type: uniqueSpecs(specs.filter((s) => s.isType).map((s) => ({ ...s, isType: false }))), - }; -} - -function importText(specs: Specifier[], quote: string, typeOnly = false): string | null { - if (specs.length === 0) return null; - const body = specs.map(formatSpec).join(", "); - return typeOnly - ? `import type { ${body} } from ${quote}react${quote};\n` - : `import { ${body} } from ${quote}react${quote};\n`; -} - -function namespaceImportText(quote: string): string { - return `import * as React from ${quote}react${quote};\n`; -} - -function quoteFor(node: SgNode): string { - const source = node.field("source") ?? node.find({ rule: { kind: "string" } }); - const text = source?.text() ?? "\"react\""; - return text.startsWith("'") ? "'" : "\""; -} - -function getReactSpecifier(importNode: SgNode): "default" | "namespace" | null { - const clause = importNode.find({ rule: { kind: "import_clause" } }); - if (!clause) return null; - const namespace = clause.find({ rule: { kind: "namespace_import" } }); - if (namespace?.find({ rule: { kind: "identifier", regex: "^React$" } })) return "namespace"; - const firstIdentifier = clause.children().find((c) => c.kind() === "identifier"); - return firstIdentifier?.text() === "React" ? "default" : null; -} - -function namedSpecifiers(importNode: SgNode): Specifier[] { - return importNode.findAll({ rule: { kind: "import_specifier" } }) - .map((spec) => ({ - name: spec.field("name")?.text() ?? spec.text(), - alias: spec.field("alias")?.text() ?? undefined, - isType: spec.text().startsWith("type ") || importNode.text().startsWith("import type"), - })); -} - -function identifierIsInImport(node: SgNode): boolean { - return node.ancestors().some((a) => a.kind() === "import_statement"); -} - -function parentUsesIdentifierAsObject(node: SgNode): SgNode | null { - const parent = node.parent(); - if (!parent) return null; - if ( - parent.kind() === "member_expression" && - parent.field("object")?.id() === node.id() && - parent.field("property") - ) { - return parent; - } - return null; -} - -function isDeclared(rootNode: SgNode, name: string): boolean { - return rootNode.findAll({ rule: { kind: "identifier", regex: `^${name}$` } }) - .some((node) => { - if (identifierIsInImport(node)) return false; - const parent = node.parent(); - if (!parent) return true; - if (parent.kind() === "member_expression" && parent.field("object")?.id() === node.id()) { - return false; - } - if (parent.kind() === "nested_type_identifier" && parent.field("object")?.id() === node.id()) { - return false; - } - return true; - }); -} - -function removeReactImportEdits(imports: SgNode[], source: string): Edit[] { - return imports.map((imp) => { - const { start, end } = statementRange(imp, source); - return { startPos: start, endPos: end, insertedText: "" }; - }); -} - -const transform: Transform = async (root, options) => { - const rootNode = root.root(); - const source = rootNode.text(); - const metric = useMetricAtom("react-import-updates"); - const edits: Edit[] = []; - - const reactImports = rootNode.findAll({ rule: { kind: "import_statement" } }).filter(isReactImport); - const valueReactImports = reactImports.filter((imp) => - !imp.text().startsWith("import type") && getReactSpecifier(imp) !== null - - ); - if (valueReactImports.length === 0) return null; - - const primaryReactImport = valueReactImports[0]!; - const reactSpecifierKind = getReactSpecifier(primaryReactImport)!; - const quote = quoteFor(primaryReactImport); - const allNamedFromReactImports = reactImports.flatMap(namedSpecifiers); - const allExistingNamed = valueReactImports.flatMap(namedSpecifiers); - const reactIdentifiers = rootNode.findAll({ rule: { kind: "identifier", regex: "^React$" } }) - .filter((node) => !identifierIsInImport(node)); - - const memberUsages: SgNode[] = []; - let hasDirectReactUse = false; - let hasTypeNamespaceUse = false; - - for (const identifier of reactIdentifiers) { - const member = parentUsesIdentifierAsObject(identifier); - if (member) { - memberUsages.push(member); - continue; - } - const parent = identifier.parent(); - if (parent?.kind() === "nested_type_identifier" && parent.field("object")?.id() === identifier.id()) { - hasTypeNamespaceUse = true; - continue; - } - hasDirectReactUse = true; - } - - const isReactUsed = reactIdentifiers.length > 0 || hasTypeNamespaceUse; - const canDestructure = isReactUsed && !hasDirectReactUse && !hasTypeNamespaceUse; - const regularSpecs: Specifier[] = []; - const typeSpecs: Specifier[] = []; - const memberReplacementEdits: Edit[] = []; - const existingRegularNames = new Set( - allNamedFromReactImports - .filter((spec) => !spec.isType) - .flatMap((spec) => [spec.name, spec.alias].filter(Boolean) as string[]), - ); - - if (canDestructure) { - for (const member of memberUsages) { - const property = member.field("property"); - if (!property) continue; - const name = property.text(); - if (existingRegularNames.has(name) || isDeclared(rootNode, name)) { - hasDirectReactUse = true; - break; - } - regularSpecs.push({ name, isType: false }); - memberReplacementEdits.push(member.replace(name)); - } - } - - const shouldDestructure = - (reactSpecifierKind === "default" || - options.params?.destructureNamespaceImports === "true") && - canDestructure && - !hasDirectReactUse; - const splitExisting = splitSpecs(allExistingNamed); - regularSpecs.push(...splitExisting.regular); - typeSpecs.push(...splitExisting.type); - - let replacementImports = ""; - if (shouldDestructure) { - edits.push(...memberReplacementEdits); - replacementImports += importText(uniqueSpecs(typeSpecs), quote, true) ?? ""; - replacementImports += importText(uniqueSpecs(regularSpecs), quote) ?? ""; - } else { - const split = splitSpecs(allExistingNamed); - replacementImports += importText(split.type, quote, true) ?? ""; - replacementImports += importText(split.regular, quote) ?? ""; - if (isReactUsed) { - replacementImports += namespaceImportText(quote); - } - } - - const firstImport = valueReactImports[0]!; - edits.push({ - startPos: firstImport.range().start.index, - endPos: firstImport.range().start.index, - insertedText: replacementImports, - }); - edits.push(...removeReactImportEdits(valueReactImports, source)); - - metric.increment({ - action: shouldDestructure - ? "convert-member-to-named" - : isReactUsed - ? "convert-to-namespace" - : "remove-react-import", - file: metricFile(root.filename()), - previous: reactSpecifierKind, - }); - - return rootNode.commitEdits(edits); -}; - -export default transform; diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/metrics.json b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/metrics.json deleted file mode 100644 index 21cc443..0000000 --- a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import-react-variable/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/default-and-multiple-specifiers-import-react-variable/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/metrics.json b/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/metrics.json deleted file mode 100644 index bd4da64..0000000 --- a/codemods/update-react-imports/tests/default-and-multiple-specifiers-import/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/default-and-multiple-specifiers-import/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/metrics.json b/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/metrics.json deleted file mode 100644 index 69f37f7..0000000 --- a/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/destructure-named-imports-react-not-removed/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/test.config.json b/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/test.config.json deleted file mode 100644 index 319e5b9..0000000 --- a/codemods/update-react-imports/tests/destructure-named-imports-react-not-removed/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "destructureNamespaceImports": true - } -} diff --git a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/input.tsx b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/input.tsx deleted file mode 100644 index ebb2ad9..0000000 --- a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/input.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from "react"; -import { createElement } from "react"; - -React.createElement('div', {}); - -
    Hi
    ; diff --git a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/metrics.json b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/metrics.json deleted file mode 100644 index cc9aca4..0000000 --- a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/destructure-named-imports-variable-used/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/test.config.json b/codemods/update-react-imports/tests/destructure-named-imports-variable-used/test.config.json deleted file mode 100644 index 319e5b9..0000000 --- a/codemods/update-react-imports/tests/destructure-named-imports-variable-used/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "destructureNamespaceImports": true - } -} diff --git a/codemods/update-react-imports/tests/destructure-named-imports/expected.tsx b/codemods/update-react-imports/tests/destructure-named-imports/expected.tsx deleted file mode 100644 index 6eea2db..0000000 --- a/codemods/update-react-imports/tests/destructure-named-imports/expected.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import * as React from "react"; -React.useState(false); - -
    \ No newline at end of file diff --git a/codemods/update-react-imports/tests/destructure-named-imports/metrics.json b/codemods/update-react-imports/tests/destructure-named-imports/metrics.json deleted file mode 100644 index 8ed6fac..0000000 --- a/codemods/update-react-imports/tests/destructure-named-imports/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/destructure-named-imports/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/destructure-named-imports/test.config.json b/codemods/update-react-imports/tests/destructure-named-imports/test.config.json deleted file mode 100644 index 319e5b9..0000000 --- a/codemods/update-react-imports/tests/destructure-named-imports/test.config.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "params": { - "destructureNamespaceImports": true - } -} diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/metrics.json b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/metrics.json deleted file mode 100644 index 7270eab..0000000 --- a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import-react-variable/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-member-to-named", - "file": "tests/flow-default-and-type-specifier-import-react-variable/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/metrics.json b/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/metrics.json deleted file mode 100644 index 5605f55..0000000 --- a/codemods/update-react-imports/tests/flow-default-and-type-specifier-import/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/flow-default-and-type-specifier-import/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/jsx-element/metrics.json b/codemods/update-react-imports/tests/jsx-element/metrics.json deleted file mode 100644 index 23bca4b..0000000 --- a/codemods/update-react-imports/tests/jsx-element/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/jsx-element/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/jsx-fragment/metrics.json b/codemods/update-react-imports/tests/jsx-fragment/metrics.json deleted file mode 100644 index 3ea49e2..0000000 --- a/codemods/update-react-imports/tests/jsx-fragment/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/jsx-fragment/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/leading-comment/metrics.json b/codemods/update-react-imports/tests/leading-comment/metrics.json deleted file mode 100644 index f30d41f..0000000 --- a/codemods/update-react-imports/tests/leading-comment/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/leading-comment/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-already-used-named-export/expected.tsx b/codemods/update-react-imports/tests/react-already-used-named-export/expected.tsx deleted file mode 100644 index 491a83a..0000000 --- a/codemods/update-react-imports/tests/react-already-used-named-export/expected.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import * as React from 'react'; -React.useState(false); \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-already-used-named-export/metrics.json b/codemods/update-react-imports/tests/react-already-used-named-export/metrics.json deleted file mode 100644 index cd15468..0000000 --- a/codemods/update-react-imports/tests/react-already-used-named-export/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/react-already-used-named-export/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/metrics.json b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/metrics.json deleted file mode 100644 index e7e8ea0..0000000 --- a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element-react-variable/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-member-to-named", - "file": "tests/react-basic-default-export-jsx-element-react-variable/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/metrics.json b/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/metrics.json deleted file mode 100644 index 7754f61..0000000 --- a/codemods/update-react-imports/tests/react-basic-default-export-jsx-element/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/react-basic-default-export-jsx-element/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-basic-default-export/metrics.json b/codemods/update-react-imports/tests/react-basic-default-export/metrics.json deleted file mode 100644 index 0a7a660..0000000 --- a/codemods/update-react-imports/tests/react-basic-default-export/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-member-to-named", - "file": "tests/react-basic-default-export/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-jsx-member-expression/metrics.json b/codemods/update-react-imports/tests/react-jsx-member-expression/metrics.json deleted file mode 100644 index e060601..0000000 --- a/codemods/update-react-imports/tests/react-jsx-member-expression/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-member-to-named", - "file": "tests/react-jsx-member-expression/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-not-removed/expected.tsx b/codemods/update-react-imports/tests/react-not-removed/expected.tsx deleted file mode 100644 index 4b9bcea..0000000 --- a/codemods/update-react-imports/tests/react-not-removed/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from "react"; -React.createElement('div', {}); - -Promise.resolve(React); - -
    Hi
    \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-not-removed/metrics.json b/codemods/update-react-imports/tests/react-not-removed/metrics.json deleted file mode 100644 index 8ab16c4..0000000 --- a/codemods/update-react-imports/tests/react-not-removed/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/react-not-removed/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-type-default-export/metrics.json b/codemods/update-react-imports/tests/react-type-default-export/metrics.json deleted file mode 100644 index 53bdfa4..0000000 --- a/codemods/update-react-imports/tests/react-type-default-export/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/react-type-default-export/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/react-type-not-removed/metrics.json b/codemods/update-react-imports/tests/react-type-not-removed/metrics.json deleted file mode 100644 index 0218dd9..0000000 --- a/codemods/update-react-imports/tests/react-type-not-removed/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/react-type-not-removed/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/metrics.json deleted file mode 100644 index fc22dae..0000000 --- a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/typescript-default-and-multiple-specifiers-import-react-variable-tsx/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/metrics.json deleted file mode 100644 index c1b188c..0000000 --- a/codemods/update-react-imports/tests/typescript-default-and-multiple-specifiers-import-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/typescript-default-and-multiple-specifiers-import-tsx/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-jsx-element-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-jsx-element-tsx/metrics.json deleted file mode 100644 index 5c3bb6b..0000000 --- a/codemods/update-react-imports/tests/typescript-jsx-element-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/typescript-jsx-element-tsx/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/metrics.json deleted file mode 100644 index 01635eb..0000000 --- a/codemods/update-react-imports/tests/typescript-jsx-fragment-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/typescript-jsx-fragment-tsx/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-leading-comment-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-leading-comment-tsx/metrics.json deleted file mode 100644 index d8e4624..0000000 --- a/codemods/update-react-imports/tests/typescript-leading-comment-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/typescript-leading-comment-tsx/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/metrics.json deleted file mode 100644 index 6918c08..0000000 --- a/codemods/update-react-imports/tests/typescript-preserve-types-default-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/typescript-preserve-types-default-tsx/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/expected.tsx deleted file mode 100644 index f9f64e1..0000000 --- a/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/expected.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import * as React from 'react'; -const App: React.FunctionComponent<{ message: string }> = ({ message }) => ( -
    {message}
    -); diff --git a/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/metrics.json deleted file mode 100644 index 6f82fd0..0000000 --- a/codemods/update-react-imports/tests/typescript-preserve-types-namespace-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/typescript-preserve-types-namespace-tsx/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/expected.tsx deleted file mode 100644 index 491a83a..0000000 --- a/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/expected.tsx +++ /dev/null @@ -1,2 +0,0 @@ -import * as React from 'react'; -React.useState(false); \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/metrics.json deleted file mode 100644 index f5ea9f4..0000000 --- a/codemods/update-react-imports/tests/typescript-react-already-used-named-export-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/typescript-react-already-used-named-export-tsx/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/metrics.json deleted file mode 100644 index bed66ce..0000000 --- a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-member-to-named", - "file": "tests/typescript-react-basic-default-export-jsx-element-react-variable-tsx/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/metrics.json deleted file mode 100644 index 5776d05..0000000 --- a/codemods/update-react-imports/tests/typescript-react-basic-default-export-jsx-element-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "remove-react-import", - "file": "tests/typescript-react-basic-default-export-jsx-element-tsx/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/metrics.json deleted file mode 100644 index 99b4fbe..0000000 --- a/codemods/update-react-imports/tests/typescript-react-basic-default-export-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-member-to-named", - "file": "tests/typescript-react-basic-default-export-tsx/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/metrics.json deleted file mode 100644 index 4224bed..0000000 --- a/codemods/update-react-imports/tests/typescript-react-jsx-member-expression-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-member-to-named", - "file": "tests/typescript-react-jsx-member-expression-tsx/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/metrics.json deleted file mode 100644 index 6acf7b7..0000000 --- a/codemods/update-react-imports/tests/typescript-react-not-removed-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/typescript-react-not-removed-tsx/input.tsx", - "previous": "default" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/expected.tsx b/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/expected.tsx deleted file mode 100644 index 48abf6f..0000000 --- a/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from "react"; -React.createElement('div', {}); - -createElement('someFunction'); - -
    Hi
    ; diff --git a/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/metrics.json b/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/metrics.json deleted file mode 100644 index 03439b9..0000000 --- a/codemods/update-react-imports/tests/typescript-variable-already-used-tsx/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/typescript-variable-already-used-tsx/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tests/variable-already-used/expected.tsx b/codemods/update-react-imports/tests/variable-already-used/expected.tsx deleted file mode 100644 index 48abf6f..0000000 --- a/codemods/update-react-imports/tests/variable-already-used/expected.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from "react"; -React.createElement('div', {}); - -createElement('someFunction'); - -
    Hi
    ; diff --git a/codemods/update-react-imports/tests/variable-already-used/metrics.json b/codemods/update-react-imports/tests/variable-already-used/metrics.json deleted file mode 100644 index cb2660e..0000000 --- a/codemods/update-react-imports/tests/variable-already-used/metrics.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "react-import-updates": [ - { - "cardinality": { - "action": "convert-to-namespace", - "file": "tests/variable-already-used/input.tsx", - "previous": "namespace" - }, - "count": 1 - } - ] -} \ No newline at end of file diff --git a/codemods/update-react-imports/tsconfig.json b/codemods/update-react-imports/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/update-react-imports/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/update-react-imports/workflow.yaml b/codemods/update-react-imports/workflow.yaml deleted file mode 100644 index f7d8d8c..0000000 --- a/codemods/update-react-imports/workflow.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -params: - schema: - destructureNamespaceImports: - name: "Destructure Namespace Imports" - description: "Allow namespace React imports to be rewritten to named imports when it is safe." - type: boolean - default: false - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/codemods/use-context-hook/.gitignore b/codemods/use-context-hook/.gitignore deleted file mode 100644 index 78174f4..0000000 --- a/codemods/use-context-hook/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# Dependencies -node_modules/ -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Build artifacts -target/ -dist/ -build/ - -# Temporary files -*.tmp -*.temp -.cache/ - -# Environment files -.env -.env.local - -# IDE files -.vscode/ -.idea/ -*.swp -*.swo - -# OS files -.DS_Store -Thumbs.db - -# Package bundles -*.tar.gz -*.tgz \ No newline at end of file diff --git a/codemods/use-context-hook/README.md b/codemods/use-context-hook/README.md deleted file mode 100644 index bf2cc57..0000000 --- a/codemods/use-context-hook/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# @react/use-context-hook - -Transform `React.useContext()` and `useContext()` calls into `use()`. - -## Usage - -```bash -codemod run @react/use-context-hook -``` - -## Development - -```bash -pnpm test -pnpm check-types -``` diff --git a/codemods/use-context-hook/pnpm-lock.yaml b/codemods/use-context-hook/pnpm-lock.yaml deleted file mode 100644 index 5bad4a8..0000000 --- a/codemods/use-context-hook/pnpm-lock.yaml +++ /dev/null @@ -1,41 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.0 - typescript: - specifier: latest - version: 5.9.3 - -packages: - - '@codemod.com/jssg-types@1.5.0': - resolution: {integrity: sha512-zChRbxI3hBSGrAHnWlEzOw1FztLWMMiarwcr0Wbk0On4hmv7dVgoUqpIHfxb64mEMKJ5syTIKY3ZNd8DcFQa5w==} - - '@jssg/utils@0.0.2': - resolution: {integrity: sha512-eCQv5Xs9yfI6OKq2PQ8SyKsxPhdiaJY/XAFJmsZiVbsK5DIBBmGBA95CsyD4JFWXDKkNv6Q7ER/Kbp6/uWzB2w==} - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - -snapshots: - - '@codemod.com/jssg-types@1.5.0': {} - - '@jssg/utils@0.0.2': {} - - typescript@5.9.3: {} diff --git a/codemods/use-context-hook/tsconfig.json b/codemods/use-context-hook/tsconfig.json deleted file mode 100644 index decec57..0000000 --- a/codemods/use-context-hook/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "lib": [ - "ESNext" - ], - "module": "NodeNext", - "moduleResolution": "NodeNext", - "types": [ - "@codemod.com/jssg-types", - "node" - ], - "allowImportingTsExtensions": true, - "noEmit": true, - "skipLibCheck": true, - "verbatimModuleSyntax": false, - "erasableSyntaxOnly": true, - "strict": true, - "strictNullChecks": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true - }, - "exclude": [ - "tests" - ] -} diff --git a/codemods/use-context-hook/workflow.yaml b/codemods/use-context-hook/workflow.yaml deleted file mode 100644 index 8773841..0000000 --- a/codemods/use-context-hook/workflow.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json - -version: "1" - -nodes: - - id: apply-transforms - name: Apply AST Transformations - type: automatic - steps: - - name: "Scan tsx files and apply fixes" - js-ast-grep: - js_file: scripts/codemod.ts - language: "tsx" diff --git a/package.json b/package.json index d721c64..933e054 100644 --- a/package.json +++ b/package.json @@ -1,32 +1,17 @@ { - "name": "jssg-react-codemods", + "name": "react-codemod-preview", "version": "0.1.0", "private": true, - "description": "React codemods built with JSSG (JavaScript ast-grep)", + "description": "Preview repo surface for React codemods with active JSSG replacements and a legacy snapshot", "type": "module", "scripts": { - "test": "pnpm -r test", - "test:use-context-hook": "pnpm --filter @react/use-context-hook test", - "test:update-react-imports": "pnpm --filter @react/update-react-imports test", - "test:replace-reactdom-render": "pnpm --filter @react/replace-reactdom-render test", - "test:replace-string-ref": "pnpm --filter @react/replace-string-ref test", - "test:rename-unsafe-lifecycles": "pnpm --filter @react/rename-unsafe-lifecycles test", - "test:error-boundaries": "pnpm --filter @react/error-boundaries test", - "test:replace-act-import": "pnpm --filter @react/replace-act-import test", - "test:replace-use-form-state": "pnpm --filter @react/replace-use-form-state test", - "test:find-dom-node": "pnpm --filter @react/find-dom-node test", - "test:react-proptypes-to-prop-types": "pnpm --filter @react/react-proptypes-to-prop-types test", - "test:pure-render-mixin": "pnpm --filter @react/pure-render-mixin test", - "test:pure-component": "pnpm --filter @react/pure-component test", - "test:sort-comp": "pnpm --filter @react/sort-comp test", - "test:manual-bind-to-arrow": "pnpm --filter @react/manual-bind-to-arrow test", - "test:create-element-to-jsx": "pnpm --filter @react/create-element-to-jsx test", - "test:remove-context-provider": "pnpm --filter @react/remove-context-provider test", - "test:remove-forward-ref": "pnpm --filter @react/remove-forward-ref test", - "test:react-dom-to-react-dom-factories": "pnpm --filter @react/react-dom-to-react-dom-factories test", - "test:react-native-view-prop-types": "pnpm --filter @react/react-native-view-prop-types test", - "test:react-to-react-dom": "pnpm --filter @react/react-to-react-dom test", - "check-types": "pnpm -r check-types" + "lint": "node ./scripts/validate-docs.mjs", + "test": "pnpm run test:active", + "test:active": "pnpm -r --filter \"@react/*\" test", + "check-types": "pnpm run check-types:active", + "check-types:active": "pnpm -r --filter \"@react/*\" check-types", + "test:legacy": "bash -lc 'cd codemods/legacy && pnpm install --ignore-workspace --frozen-lockfile --force && pnpm run test:ci'", + "ci": "pnpm run lint && pnpm run test:active && pnpm run check-types:active && pnpm run test:legacy" }, "keywords": [ "codemod", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17dc5f9..5468347 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,115 +16,7 @@ importers: specifier: latest version: 25.6.0 - codemods/create-element-to-jsx: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/error-boundaries: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/find-dom-node: - dependencies: - '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/manual-bind-to-arrow: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/pure-component: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/pure-render-mixin: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/react-dom-to-react-dom-factories: - dependencies: - '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/react-native-view-prop-types: - dependencies: - '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/react-proptypes-to-prop-types: + codemods/jssg/react-proptypes-to-prop-types: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -140,31 +32,7 @@ importers: specifier: latest version: 6.0.2 - codemods/react-to-react-dom: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/remove-context-provider: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/remove-forward-ref: + codemods/jssg/replace-act-import: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -180,35 +48,7 @@ importers: specifier: latest version: 6.0.2 - codemods/rename-unsafe-lifecycles: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/replace-act-import: - dependencies: - '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/replace-reactdom-render: + codemods/jssg/replace-reactdom-render: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -224,7 +64,7 @@ importers: specifier: latest version: 6.0.2 - codemods/replace-string-ref: + codemods/jssg/replace-string-ref: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -240,35 +80,7 @@ importers: specifier: latest version: 6.0.2 - codemods/replace-use-form-state: - dependencies: - '@jssg/utils': - specifier: ^0.0.2 - version: 0.0.2 - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/sort-comp: - devDependencies: - '@codemod.com/jssg-types': - specifier: latest - version: 1.5.1 - '@types/node': - specifier: latest - version: 25.6.0 - typescript: - specifier: latest - version: 6.0.2 - - codemods/update-react-imports: + codemods/jssg/replace-use-form-state: dependencies: '@jssg/utils': specifier: ^0.0.2 @@ -284,7 +96,7 @@ importers: specifier: latest version: 6.0.2 - codemods/use-context-hook: + codemods/jssg/use-context-hook: dependencies: '@jssg/utils': specifier: ^0.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 83b59d2..15e528b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,2 @@ packages: - - "codemods/*" + - "codemods/jssg/*" diff --git a/scripts/validate-docs.mjs b/scripts/validate-docs.mjs new file mode 100644 index 0000000..29805a6 --- /dev/null +++ b/scripts/validate-docs.mjs @@ -0,0 +1,33 @@ +import fs from "node:fs"; +import path from "node:path"; + +const repoRoot = process.cwd(); +const rootReadme = fs.readFileSync(path.join(repoRoot, "README.md"), "utf8"); +const legacyReadme = fs.readFileSync(path.join(repoRoot, "LEGACY.md"), "utf8"); + +const forbiddenRootPatterns = [ + { label: "Codemod Registry command claim", regex: /npx codemod react\// }, + { label: "package-style codemod run claim", regex: /codemod run @react\// }, +]; + +for (const pattern of forbiddenRootPatterns) { + if (pattern.regex.test(rootReadme)) { + throw new Error(`README.md contains forbidden ${pattern.label}.`); + } +} + +if (/Codemod Registry/.test(legacyReadme) && /npx codemod react\//.test(legacyReadme)) { + throw new Error("LEGACY.md must not claim legacy transforms are runnable via Codemod Registry."); +} + +const transformRoot = path.join(repoRoot, "codemods", "jssg"); +for (const name of fs.readdirSync(transformRoot)) { + const readmePath = path.join(transformRoot, name, "README.md"); + const contents = fs.readFileSync(readmePath, "utf8"); + + if (/codemod run @react\//.test(contents) || /npx codemod react\//.test(contents)) { + throw new Error(`${path.relative(repoRoot, readmePath)} contains a forbidden published-run claim.`); + } +} + +console.log("Documentation validation passed."); From 2e3b434aad071ac15e01fbc85fbf075b1f34bc33 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 20:42:25 +0200 Subject: [PATCH 22/29] update docs --- LEGACY.md | 50 ++++++++++++---------------- PARITY_STATUS.md | 28 ++++++++-------- README.md | 85 ++++++++++++++++++++++++++++++++++++++---------- package.json | 4 +-- 4 files changed, 103 insertions(+), 64 deletions(-) diff --git a/LEGACY.md b/LEGACY.md index b815337..730dcb0 100644 --- a/LEGACY.md +++ b/LEGACY.md @@ -1,40 +1,32 @@ # Legacy Codemods -This branch keeps the non-high-priority `react-codemod` transforms as a legacy jscodeshift snapshot under [`codemods/legacy/transforms/`](./codemods/legacy/transforms/). +The following jscodeshift-based codemods from the original [`react-codemod`](https://github.com/reactjs/react-codemod) project are available under [`codemods/legacy/transforms/`](./codemods/legacy/transforms/). -The legacy snapshot is preserved for source reference, upstream-style tests, and branch preview parity with the structure of `reactjs/react-codemod`. +These codemods are preserved for compatibility and can be run directly with jscodeshift. -## Legacy Catalog +## Catalog -- [`create-element-to-jsx`](./codemods/legacy/transforms/create-element-to-jsx.js) -- [`error-boundaries`](./codemods/legacy/transforms/error-boundaries.js) -- [`findDOMNode`](./codemods/legacy/transforms/findDOMNode.js) -- [`manual-bind-to-arrow`](./codemods/legacy/transforms/manual-bind-to-arrow.js) -- [`pure-component`](./codemods/legacy/transforms/pure-component.js) -- [`pure-render-mixin`](./codemods/legacy/transforms/pure-render-mixin.js) -- [`React-DOM-to-react-dom-factories`](./codemods/legacy/transforms/React-DOM-to-react-dom-factories.js) -- [`ReactNative-View-propTypes`](./codemods/legacy/transforms/ReactNative-View-propTypes.js) -- [`react-to-react-dom`](./codemods/legacy/transforms/react-to-react-dom.js) -- [`remove-context-provider`](./codemods/legacy/transforms/remove-context-provider.ts) -- [`remove-forward-ref`](./codemods/legacy/transforms/remove-forward-ref.ts) -- [`rename-unsafe-lifecycles`](./codemods/legacy/transforms/rename-unsafe-lifecycles.js) -- [`sort-comp`](./codemods/legacy/transforms/sort-comp.js) -- [`update-react-imports`](./codemods/legacy/transforms/update-react-imports.js) -- [`class`](./codemods/legacy/transforms/class.js) +- [`create-element-to-jsx`](./codemods/legacy/transforms/create-element-to-jsx.js) — convert `React.createElement` calls to JSX +- [`error-boundaries`](./codemods/legacy/transforms/error-boundaries.js) — rename `unstable_handleError` to `componentDidCatch` +- [`findDOMNode`](./codemods/legacy/transforms/findDOMNode.js) — update `getDOMNode()` calls to `React.findDOMNode()` +- [`manual-bind-to-arrow`](./codemods/legacy/transforms/manual-bind-to-arrow.js) — convert manual function bindings to arrow functions +- [`pure-component`](./codemods/legacy/transforms/pure-component.js) — convert render-only class components to functional components +- [`pure-render-mixin`](./codemods/legacy/transforms/pure-render-mixin.js) — remove `PureRenderMixin` and inline `shouldComponentUpdate` +- [`React-DOM-to-react-dom-factories`](./codemods/legacy/transforms/React-DOM-to-react-dom-factories.js) — convert `React.DOM.div(...)` to `React.createElement('div', ...)` +- [`ReactNative-View-propTypes`](./codemods/legacy/transforms/ReactNative-View-propTypes.js) — replace `View.propTypes` with `ViewPropTypes` +- [`react-to-react-dom`](./codemods/legacy/transforms/react-to-react-dom.js) — update code for the `react` / `react-dom` package split +- [`remove-context-provider`](./codemods/legacy/transforms/remove-context-provider.ts) — convert `Context.Provider` elements to `Context` +- [`remove-forward-ref`](./codemods/legacy/transforms/remove-forward-ref.ts) — remove usages of `forwardRef` +- [`rename-unsafe-lifecycles`](./codemods/legacy/transforms/rename-unsafe-lifecycles.js) — add `UNSAFE_` prefix to deprecated lifecycle hooks +- [`sort-comp`](./codemods/legacy/transforms/sort-comp.js) — enforce React component method ordering +- [`update-react-imports`](./codemods/legacy/transforms/update-react-imports.js) — remove redundant React imports and convert to named imports +- [`class`](./codemods/legacy/transforms/class.js) — convert `React.createClass` calls to ES6 classes -## Notes +## Running -- The 6 promoted JSSG transforms are not duplicated in `codemods/legacy/`. -- This preview branch does not claim that legacy transforms can be run through Codemod Registry. -- The legacy snapshot keeps the full upstream-style test corpus in `codemods/legacy/transforms/__tests__`. -- The preview branch CI runs the compatible legacy subset under the current preview runtime: - -```bash -pnpm run test:legacy -``` - -- To run the full legacy snapshot test corpus manually from `codemods/legacy/`: +From `codemods/legacy/`: ```bash +pnpm install pnpm test ``` diff --git a/PARITY_STATUS.md b/PARITY_STATUS.md index 4271f0c..328e542 100644 --- a/PARITY_STATUS.md +++ b/PARITY_STATUS.md @@ -1,28 +1,26 @@ -# Legacy Parity Status +# Parity Status Last updated: 2026-04-15 Status meanings: -- `Certified`: replacement-grade rollout confidence. Strict/default package tests are green, public test posture is portable, and there are no known rollout-blocking logic gaps versus the legacy jscodeshift codemod. `Certified` may include intentional safe extensions on legacy-unsupported edge cases, as long as intentional legacy behavior remains unbroken. -- `Legacy Snapshot`: preserved as upstream jscodeshift source and tests on this branch, but not promoted as part of the active JSSG replacement surface. +- `Certified`: replacement-grade confidence. Tests are green, public test posture is portable, and there are no known logic gaps versus the original jscodeshift codemod. May include safe extensions on edge cases the original did not handle, as long as original behavior remains unbroken. +- `Legacy`: available as a jscodeshift codemod under `codemods/legacy/`. Not yet ported to JSSG. -## Active JSSG Surface +## JSSG Codemods -These are the only codemods promoted as active replacements on this branch. - -| Codemod | Legacy Source | Status | Notes | +| Codemod | Original Source | Status | Notes | | --- | --- | --- | --- | -| `replace-use-form-state` | `replace-use-form-state.ts` | `Certified` | Legacy fixture surface plus added multi-import regression coverage are green. Collection-wide replacement on matching `react-dom` imports fixes a real parity gap, and the broader multi-import handling is treated as an acceptable safe extension where legacy only partially migrated files. | -| `replace-act-import` | `replace-act-import.ts` | `Certified` | Legacy fixture surface plus added multi-import regression coverage. This closes a real parity gap, and the mixed test-utils partial-removal path is an acceptable safe extension that does not break intentional legacy behavior. | -| `replace-string-ref` | `replace-string-ref.ts` | `Certified` | Legacy fixture surface plus added namespace/default-export/multi-ref coverage are green, and the direct-superclass guard restores the intended direct-superclass behavior by avoiding the prior mixin false positive. | -| `replace-reactdom-render` | `replace-reactdom-render.ts` | `Certified` | Legacy fixtures plus added multi-alias regression coverage are green. The extra multi-alias handling is an acceptable safe extension over legacy implementation limits; it preserves intentional legacy behavior while covering additional valid cases. | -| `react-proptypes-to-prop-types` | `React-PropTypes-to-prop-types.js` | `Certified` | Full legacy fixture surface is green, targeted mixed-import validation is aligned, and no JSSG-specific rollout blocker was found in the audit. No additional extension beyond the legacy behavior is currently relied on for certification. | -| `use-context-hook` | `use-context-hook.ts` | `Certified` | Legacy fixture surface plus added multi-import regression coverage are green. The multi-import handling goes beyond the legacy implementation, but it is treated as an acceptable safe extension because it preserves intentional legacy behavior while covering additional valid cases. | +| `replace-use-form-state` | `replace-use-form-state.ts` | `Certified` | Full fixture coverage plus multi-import regression tests are green. Collection-wide replacement on matching `react-dom` imports fixes a real parity gap. | +| `replace-act-import` | `replace-act-import.ts` | `Certified` | Full fixture coverage plus multi-import regression tests. Mixed test-utils partial-removal path is a safe extension. | +| `replace-string-ref` | `replace-string-ref.ts` | `Certified` | Full fixture coverage plus namespace/default-export/multi-ref tests are green. Direct-superclass guard restores intended behavior. | +| `replace-reactdom-render` | `replace-reactdom-render.ts` | `Certified` | Full fixture coverage plus multi-alias regression tests are green. | +| `react-proptypes-to-prop-types` | `React-PropTypes-to-prop-types.js` | `Certified` | Full original fixture surface is green. No JSSG-specific rollout blocker found. | +| `use-context-hook` | `use-context-hook.ts` | `Certified` | Full fixture coverage plus multi-import regression tests are green. | -## Legacy Snapshot Surface +## Legacy Codemods -These codemods remain available only as an upstream jscodeshift snapshot under [`codemods/legacy/transforms/`](./codemods/legacy/transforms/): +These codemods are available as jscodeshift transforms under [`codemods/legacy/transforms/`](./codemods/legacy/transforms/): - `class` - `create-element-to-jsx` diff --git a/README.md b/README.md index a1af4ef..9f57ed4 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,61 @@ -# React Codemods Preview +# React Codemods -This branch is a React-facing preview of how this repo should look as a replacement-style home for `react-codemod`. +This repository contains a collection of codemods to help update React apps. -It promotes 6 audited JSSG codemods as the active surface and preserves the remaining upstream jscodeshift codemods as a legacy snapshot. +All codemods are free and open source, with the source code available in this repository. -## Active JSSG Transforms +## Usage -These transforms are the active replacement surface on this branch. They are source-first JSSG codemods and are not described here as Codemod Registry entries unless they are actually published. +Run a codemod from this directory using the [Codemod CLI](https://go.codemod.com/cli-docs): -- [`use-context-hook`](./codemods/jssg/use-context-hook/) — replace `useContext` usage with `use` -- [`replace-act-import`](./codemods/jssg/replace-act-import/) — move `act` from `react-dom/test-utils` to `react` -- [`replace-string-ref`](./codemods/jssg/replace-string-ref/) — replace deprecated string refs with callback refs -- [`replace-use-form-state`](./codemods/jssg/replace-use-form-state/) — rename `useFormState` to `useActionState` -- [`replace-reactdom-render`](./codemods/jssg/replace-reactdom-render/) — replace `ReactDOM.render` with `createRoot(...).render(...)` -- [`react-proptypes-to-prop-types`](./codemods/jssg/react-proptypes-to-prop-types/) — replace `React.PropTypes` with `prop-types` +```bash +pnpm dlx codemod@latest jssg run --language tsx --target ./codemods/jssg//scripts/codemod.ts +``` + +- `` — name of the codemod (see available codemods below) +- `` — files or directory to transform + +## Available Codemods + +### `use-context-hook` + +Replaces usages of `React.useContext(...)` with `React.use(...)`. + +See [`use-context-hook`](./codemods/jssg/use-context-hook/) for details. + +### `replace-act-import` + +Updates `act` import path from `react-dom/test-utils` to `react`. + +See [`replace-act-import`](./codemods/jssg/replace-act-import/) for details. + +### `replace-string-ref` + +Replaces deprecated string refs with callback refs. + +See [`replace-string-ref`](./codemods/jssg/replace-string-ref/) for details. + +### `replace-use-form-state` + +Replaces usages of `useFormState()` with `useActionState()`. -## Legacy Codemods +See [`replace-use-form-state`](./codemods/jssg/replace-use-form-state/) for details. -All non-priority transforms are preserved as an upstream-style legacy jscodeshift snapshot under [`codemods/legacy/`](./codemods/legacy/). +### `replace-reactdom-render` -See [LEGACY.md](./LEGACY.md) for the legacy catalog and testing notes. +Replaces usages of `ReactDOM.render()` with `createRoot(node).render()`. + +See [`replace-reactdom-render`](./codemods/jssg/replace-reactdom-render/) for details. + +### `react-proptypes-to-prop-types` + +Replaces `React.PropTypes` references with the `prop-types` package and adds the appropriate import statement. + +See [`react-proptypes-to-prop-types`](./codemods/jssg/react-proptypes-to-prop-types/) for details. + +### Legacy Codemods + +Additional jscodeshift-based codemods from the original `react-codemod` project are available under [`codemods/legacy/`](./codemods/legacy/). See [LEGACY.md](./LEGACY.md) for the full catalog. ## Development @@ -28,15 +64,28 @@ pnpm install pnpm run ci ``` -Run active JSSG checks only: +Run tests: + +```bash +pnpm test +``` + +Run type checking: ```bash -pnpm run test:active -pnpm run check-types:active +pnpm run check-types ``` -Run the legacy snapshot test harness: +Run legacy codemod tests: ```bash pnpm run test:legacy ``` + +## Support and Contributing + +If you want to contribute, you're welcome to submit a pull request. + +## License + +react-codemod is [MIT licensed](./LICENSE). diff --git a/package.json b/package.json index 933e054..a439355 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "react-codemod-preview", + "name": "react-codemod", "version": "0.1.0", "private": true, - "description": "Preview repo surface for React codemods with active JSSG replacements and a legacy snapshot", + "description": "A collection of codemods to help update React apps", "type": "module", "scripts": { "lint": "node ./scripts/validate-docs.mjs", From b1a0069750600536abab7ff89fa1f16f7c6339d0 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 21:13:07 +0200 Subject: [PATCH 23/29] add migration recipe --- README.md | 6 ++++ .../jssg/react-19-migration-recipe/README.md | 17 +++++++++++ .../react-19-migration-recipe/codemod.yaml | 19 +++++++++++++ .../react-19-migration-recipe/package.json | 8 ++++++ .../react-19-migration-recipe/workflow.yaml | 28 +++++++++++++++++++ pnpm-lock.yaml | 2 ++ 6 files changed, 80 insertions(+) create mode 100644 codemods/jssg/react-19-migration-recipe/README.md create mode 100644 codemods/jssg/react-19-migration-recipe/codemod.yaml create mode 100644 codemods/jssg/react-19-migration-recipe/package.json create mode 100644 codemods/jssg/react-19-migration-recipe/workflow.yaml diff --git a/README.md b/README.md index 9f57ed4..02fe964 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,12 @@ Replaces `React.PropTypes` references with the `prop-types` package and adds the See [`react-proptypes-to-prop-types`](./codemods/jssg/react-proptypes-to-prop-types/) for details. +### `react-19-migration-recipe` + +Runs all React 19 migration codemods in sequence: `replace-reactdom-render`, `replace-string-ref`, `replace-act-import`, `replace-use-form-state`, and `use-context-hook`. + +See [`react-19-migration-recipe`](./codemods/jssg/react-19-migration-recipe/) for details. + ### Legacy Codemods Additional jscodeshift-based codemods from the original `react-codemod` project are available under [`codemods/legacy/`](./codemods/legacy/). See [LEGACY.md](./LEGACY.md) for the full catalog. diff --git a/codemods/jssg/react-19-migration-recipe/README.md b/codemods/jssg/react-19-migration-recipe/README.md new file mode 100644 index 0000000..79c00f2 --- /dev/null +++ b/codemods/jssg/react-19-migration-recipe/README.md @@ -0,0 +1,17 @@ +# react-19-migration-recipe + +Run all React 19 migration codemods in sequence. + +This recipe applies the following codemods: + +1. **replace-reactdom-render** — replace `ReactDOM.render` with `createRoot(...).render(...)` +2. **replace-string-ref** — replace deprecated string refs with callback refs +3. **replace-act-import** — move `act` from `react-dom/test-utils` to `react` +4. **replace-use-form-state** — rename `useFormState` to `useActionState` +5. **use-context-hook** — replace `useContext` with `use` + +## Run Locally + +```bash +pnpm dlx codemod@latest workflow run ./workflow.yaml --target +``` diff --git a/codemods/jssg/react-19-migration-recipe/codemod.yaml b/codemods/jssg/react-19-migration-recipe/codemod.yaml new file mode 100644 index 0000000..499ae1b --- /dev/null +++ b/codemods/jssg/react-19-migration-recipe/codemod.yaml @@ -0,0 +1,19 @@ +schema_version: "1.0" + +name: "@react/react-19-migration-recipe" +version: "0.1.0" +description: "Run all React 19 migration codemods in sequence" +author: "Codemod " +license: "MIT" +workflow: "workflow.yaml" + +targets: + languages: ["tsx"] + +keywords: ["React", "migration", "recipe"] + +registry: + access: "public" + visibility: "public" + +capabilities: [] diff --git a/codemods/jssg/react-19-migration-recipe/package.json b/codemods/jssg/react-19-migration-recipe/package.json new file mode 100644 index 0000000..3d54e0f --- /dev/null +++ b/codemods/jssg/react-19-migration-recipe/package.json @@ -0,0 +1,8 @@ +{ + "name": "@react/react-19-migration-recipe", + "version": "0.1.0", + "description": "Run all React 19 migration codemods in sequence", + "type": "module", + "private": true, + "license": "MIT" +} diff --git a/codemods/jssg/react-19-migration-recipe/workflow.yaml b/codemods/jssg/react-19-migration-recipe/workflow.yaml new file mode 100644 index 0000000..dfbfd95 --- /dev/null +++ b/codemods/jssg/react-19-migration-recipe/workflow.yaml @@ -0,0 +1,28 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: react-19-migration + name: React 19 Migration + type: automatic + steps: + - name: "Replace ReactDOM.render with createRoot" + codemod: + source: "@react/replace-reactdom-render" + + - name: "Replace string refs with callback refs" + codemod: + source: "@react/replace-string-ref" + + - name: "Migrate act from react-dom/test-utils to react" + codemod: + source: "@react/replace-act-import" + + - name: "Replace useFormState with useActionState" + codemod: + source: "@react/replace-use-form-state" + + - name: "Replace useContext with use" + codemod: + source: "@react/use-context-hook" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5468347..92bc53d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,6 +16,8 @@ importers: specifier: latest version: 25.6.0 + codemods/jssg/react-19-migration-recipe: {} + codemods/jssg/react-proptypes-to-prop-types: dependencies: '@jssg/utils': From 9f24bcaa4563ae066949049a692eae1a7698f23c Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 21:28:16 +0200 Subject: [PATCH 24/29] update codemod names --- README.md | 64 ++++++++++++++----- .../jssg/react-19-migration-recipe/README.md | 12 ++-- .../react-19-migration-recipe/codemod.yaml | 4 +- .../react-19-migration-recipe/package.json | 2 +- .../react-19-migration-recipe/workflow.yaml | 10 +-- .../react-proptypes-to-prop-types/README.md | 6 +- .../codemod.yaml | 4 +- .../package.json | 2 +- codemods/jssg/replace-act-import/README.md | 6 +- codemods/jssg/replace-act-import/codemod.yaml | 4 +- codemods/jssg/replace-act-import/package.json | 2 +- .../jssg/replace-reactdom-render/README.md | 6 +- .../jssg/replace-reactdom-render/codemod.yaml | 4 +- .../jssg/replace-reactdom-render/package.json | 2 +- codemods/jssg/replace-string-ref/README.md | 6 +- codemods/jssg/replace-string-ref/codemod.yaml | 4 +- codemods/jssg/replace-string-ref/package.json | 2 +- .../jssg/replace-use-form-state/README.md | 6 +- .../jssg/replace-use-form-state/codemod.yaml | 4 +- .../jssg/replace-use-form-state/package.json | 2 +- codemods/jssg/use-context-hook/README.md | 6 +- codemods/jssg/use-context-hook/codemod.yaml | 4 +- codemods/jssg/use-context-hook/package.json | 2 +- package.json | 4 +- scripts/validate-docs.mjs | 6 +- 25 files changed, 97 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 02fe964..0f21006 100644 --- a/README.md +++ b/README.md @@ -6,60 +6,92 @@ All codemods are free and open source, with the source code available in this re ## Usage -Run a codemod from this directory using the [Codemod CLI](https://go.codemod.com/cli-docs): +We recommend using the [`codemod`](https://go.codemod.com/github) command for running codemods. ```bash -pnpm dlx codemod@latest jssg run --language tsx --target ./codemods/jssg//scripts/codemod.ts +npx codemod --target ``` -- `` — name of the codemod (see available codemods below) +- `` — name of the codemod (see available codemods below) - `` — files or directory to transform +Check [codemod docs](https://go.codemod.com/cli-docs) for the full list of available commands. + ## Available Codemods -### `use-context-hook` +All React codemods are also available in the [Codemod Registry](https://go.codemod.com/react-codemods). + +#### `react-19-migration-recipe` + +Runs all React 19 migration codemods in sequence. + +```bash +npx codemod @react-new/react-19-migration-recipe --target +``` + +See [`react-19-migration-recipe`](./codemods/jssg/react-19-migration-recipe/) for details. + +#### `use-context-hook` Replaces usages of `React.useContext(...)` with `React.use(...)`. +```bash +npx codemod @react-new/use-context-hook --target +``` + See [`use-context-hook`](./codemods/jssg/use-context-hook/) for details. -### `replace-act-import` +#### `replace-act-import` Updates `act` import path from `react-dom/test-utils` to `react`. +```bash +npx codemod @react-new/replace-act-import --target +``` + See [`replace-act-import`](./codemods/jssg/replace-act-import/) for details. -### `replace-string-ref` +#### `replace-string-ref` Replaces deprecated string refs with callback refs. +```bash +npx codemod @react-new/replace-string-ref --target +``` + See [`replace-string-ref`](./codemods/jssg/replace-string-ref/) for details. -### `replace-use-form-state` +#### `replace-use-form-state` Replaces usages of `useFormState()` with `useActionState()`. +```bash +npx codemod @react-new/replace-use-form-state --target +``` + See [`replace-use-form-state`](./codemods/jssg/replace-use-form-state/) for details. -### `replace-reactdom-render` +#### `replace-reactdom-render` Replaces usages of `ReactDOM.render()` with `createRoot(node).render()`. +```bash +npx codemod @react-new/replace-reactdom-render --target +``` + See [`replace-reactdom-render`](./codemods/jssg/replace-reactdom-render/) for details. -### `react-proptypes-to-prop-types` +#### `react-proptypes-to-prop-types` Replaces `React.PropTypes` references with the `prop-types` package and adds the appropriate import statement. -See [`react-proptypes-to-prop-types`](./codemods/jssg/react-proptypes-to-prop-types/) for details. - -### `react-19-migration-recipe` - -Runs all React 19 migration codemods in sequence: `replace-reactdom-render`, `replace-string-ref`, `replace-act-import`, `replace-use-form-state`, and `use-context-hook`. +```bash +npx codemod @react-new/react-proptypes-to-prop-types --target +``` -See [`react-19-migration-recipe`](./codemods/jssg/react-19-migration-recipe/) for details. +See [`react-proptypes-to-prop-types`](./codemods/jssg/react-proptypes-to-prop-types/) for details. -### Legacy Codemods +#### Legacy Codemods Additional jscodeshift-based codemods from the original `react-codemod` project are available under [`codemods/legacy/`](./codemods/legacy/). See [LEGACY.md](./LEGACY.md) for the full catalog. diff --git a/codemods/jssg/react-19-migration-recipe/README.md b/codemods/jssg/react-19-migration-recipe/README.md index 79c00f2..fecdd48 100644 --- a/codemods/jssg/react-19-migration-recipe/README.md +++ b/codemods/jssg/react-19-migration-recipe/README.md @@ -2,6 +2,12 @@ Run all React 19 migration codemods in sequence. +## Usage + +```bash +npx codemod @react-new/react-19-migration-recipe --target +``` + This recipe applies the following codemods: 1. **replace-reactdom-render** — replace `ReactDOM.render` with `createRoot(...).render(...)` @@ -9,9 +15,3 @@ This recipe applies the following codemods: 3. **replace-act-import** — move `act` from `react-dom/test-utils` to `react` 4. **replace-use-form-state** — rename `useFormState` to `useActionState` 5. **use-context-hook** — replace `useContext` with `use` - -## Run Locally - -```bash -pnpm dlx codemod@latest workflow run ./workflow.yaml --target -``` diff --git a/codemods/jssg/react-19-migration-recipe/codemod.yaml b/codemods/jssg/react-19-migration-recipe/codemod.yaml index 499ae1b..aff2ebe 100644 --- a/codemods/jssg/react-19-migration-recipe/codemod.yaml +++ b/codemods/jssg/react-19-migration-recipe/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" -name: "@react/react-19-migration-recipe" +name: "@react-new/react-19-migration-recipe" version: "0.1.0" description: "Run all React 19 migration codemods in sequence" author: "Codemod " @@ -14,6 +14,6 @@ keywords: ["React", "migration", "recipe"] registry: access: "public" - visibility: "public" + visibility: "private" capabilities: [] diff --git a/codemods/jssg/react-19-migration-recipe/package.json b/codemods/jssg/react-19-migration-recipe/package.json index 3d54e0f..3579557 100644 --- a/codemods/jssg/react-19-migration-recipe/package.json +++ b/codemods/jssg/react-19-migration-recipe/package.json @@ -1,5 +1,5 @@ { - "name": "@react/react-19-migration-recipe", + "name": "@react-new/react-19-migration-recipe", "version": "0.1.0", "description": "Run all React 19 migration codemods in sequence", "type": "module", diff --git a/codemods/jssg/react-19-migration-recipe/workflow.yaml b/codemods/jssg/react-19-migration-recipe/workflow.yaml index dfbfd95..0d48a99 100644 --- a/codemods/jssg/react-19-migration-recipe/workflow.yaml +++ b/codemods/jssg/react-19-migration-recipe/workflow.yaml @@ -9,20 +9,20 @@ nodes: steps: - name: "Replace ReactDOM.render with createRoot" codemod: - source: "@react/replace-reactdom-render" + source: "@react-new/replace-reactdom-render" - name: "Replace string refs with callback refs" codemod: - source: "@react/replace-string-ref" + source: "@react-new/replace-string-ref" - name: "Migrate act from react-dom/test-utils to react" codemod: - source: "@react/replace-act-import" + source: "@react-new/replace-act-import" - name: "Replace useFormState with useActionState" codemod: - source: "@react/replace-use-form-state" + source: "@react-new/replace-use-form-state" - name: "Replace useContext with use" codemod: - source: "@react/use-context-hook" + source: "@react-new/use-context-hook" diff --git a/codemods/jssg/react-proptypes-to-prop-types/README.md b/codemods/jssg/react-proptypes-to-prop-types/README.md index 5ac582a..f2e6b9a 100644 --- a/codemods/jssg/react-proptypes-to-prop-types/README.md +++ b/codemods/jssg/react-proptypes-to-prop-types/README.md @@ -2,12 +2,10 @@ Replace legacy React `PropTypes` usage with the `prop-types` package across imports, requires, aliases, and destructuring patterns. -## Run Locally - -Run this command from this directory: +## Usage ```bash -pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +npx codemod @react-new/react-proptypes-to-prop-types --target ``` To override the `prop-types` module specifier, pass `--param module-name=`. diff --git a/codemods/jssg/react-proptypes-to-prop-types/codemod.yaml b/codemods/jssg/react-proptypes-to-prop-types/codemod.yaml index e68b5c5..20814bf 100644 --- a/codemods/jssg/react-proptypes-to-prop-types/codemod.yaml +++ b/codemods/jssg/react-proptypes-to-prop-types/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" -name: "@react/react-proptypes-to-prop-types" +name: "@react-new/react-proptypes-to-prop-types" version: "0.1.0" description: "Replace React.PropTypes with prop-types package" author: "Codemod " @@ -14,6 +14,6 @@ keywords: ["React"] registry: access: "public" - visibility: "public" + visibility: "private" capabilities: [] diff --git a/codemods/jssg/react-proptypes-to-prop-types/package.json b/codemods/jssg/react-proptypes-to-prop-types/package.json index 51a88ef..3eb93eb 100644 --- a/codemods/jssg/react-proptypes-to-prop-types/package.json +++ b/codemods/jssg/react-proptypes-to-prop-types/package.json @@ -1,5 +1,5 @@ { - "name": "@react/react-proptypes-to-prop-types", + "name": "@react-new/react-proptypes-to-prop-types", "version": "0.1.0", "description": "Replace React.PropTypes with prop-types package", "type": "module", diff --git a/codemods/jssg/replace-act-import/README.md b/codemods/jssg/replace-act-import/README.md index 96cfdb4..6406bed 100644 --- a/codemods/jssg/replace-act-import/README.md +++ b/codemods/jssg/replace-act-import/README.md @@ -2,12 +2,10 @@ Move `act` usage from `react-dom/test-utils` to `react` across named imports, namespace imports, and re-exports. -## Run Locally - -Run this command from this directory: +## Usage ```bash -pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +npx codemod @react-new/replace-act-import --target ``` ## Development diff --git a/codemods/jssg/replace-act-import/codemod.yaml b/codemods/jssg/replace-act-import/codemod.yaml index 1570066..7e3a99e 100644 --- a/codemods/jssg/replace-act-import/codemod.yaml +++ b/codemods/jssg/replace-act-import/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" -name: "@react/replace-act-import" +name: "@react-new/replace-act-import" version: "0.1.0" description: "Migrate act() from react-dom/test-utils to react" author: "Codemod " @@ -14,6 +14,6 @@ keywords: ["React"] registry: access: "public" - visibility: "public" + visibility: "private" capabilities: [] diff --git a/codemods/jssg/replace-act-import/package.json b/codemods/jssg/replace-act-import/package.json index bc9cda9..da11999 100644 --- a/codemods/jssg/replace-act-import/package.json +++ b/codemods/jssg/replace-act-import/package.json @@ -1,5 +1,5 @@ { - "name": "@react/replace-act-import", + "name": "@react-new/replace-act-import", "version": "0.1.0", "description": "Migrate act() from react-dom/test-utils to react", "type": "module", diff --git a/codemods/jssg/replace-reactdom-render/README.md b/codemods/jssg/replace-reactdom-render/README.md index 4d8224c..6bfd52f 100644 --- a/codemods/jssg/replace-reactdom-render/README.md +++ b/codemods/jssg/replace-reactdom-render/README.md @@ -2,12 +2,10 @@ Replace `ReactDOM.render` and named `render` imports with `createRoot(...).render(...)`. -## Run Locally - -Run this command from this directory: +## Usage ```bash -pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +npx codemod @react-new/replace-reactdom-render --target ``` ## Development diff --git a/codemods/jssg/replace-reactdom-render/codemod.yaml b/codemods/jssg/replace-reactdom-render/codemod.yaml index b2b3bcf..ce8a58c 100644 --- a/codemods/jssg/replace-reactdom-render/codemod.yaml +++ b/codemods/jssg/replace-reactdom-render/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" -name: "@react/replace-reactdom-render" +name: "@react-new/replace-reactdom-render" version: "0.1.0" description: "Replace ReactDOM.render with createRoot for React 18" author: "Codemod " @@ -15,6 +15,6 @@ keywords: ["React"] registry: access: "public" - visibility: "public" + visibility: "private" capabilities: [] diff --git a/codemods/jssg/replace-reactdom-render/package.json b/codemods/jssg/replace-reactdom-render/package.json index f6deff3..053a994 100644 --- a/codemods/jssg/replace-reactdom-render/package.json +++ b/codemods/jssg/replace-reactdom-render/package.json @@ -1,5 +1,5 @@ { - "name": "@react/replace-reactdom-render", + "name": "@react-new/replace-reactdom-render", "version": "0.1.0", "description": "Replace ReactDOM.render with createRoot for React 18", "type": "module", diff --git a/codemods/jssg/replace-string-ref/README.md b/codemods/jssg/replace-string-ref/README.md index 37ed12d..4a9267d 100644 --- a/codemods/jssg/replace-string-ref/README.md +++ b/codemods/jssg/replace-string-ref/README.md @@ -2,12 +2,10 @@ Replace string refs in React class components with callback refs that assign through `this.refs`. -## Run Locally - -Run this command from this directory: +## Usage ```bash -pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +npx codemod @react-new/replace-string-ref --target ``` ## Development diff --git a/codemods/jssg/replace-string-ref/codemod.yaml b/codemods/jssg/replace-string-ref/codemod.yaml index a28823c..f9fe55c 100644 --- a/codemods/jssg/replace-string-ref/codemod.yaml +++ b/codemods/jssg/replace-string-ref/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" -name: "@react/replace-string-ref" +name: "@react-new/replace-string-ref" version: "0.1.0" description: "Replace string refs with callback refs in React class components" author: "Codemod " @@ -15,6 +15,6 @@ keywords: ["React"] registry: access: "public" - visibility: "public" + visibility: "private" capabilities: [] diff --git a/codemods/jssg/replace-string-ref/package.json b/codemods/jssg/replace-string-ref/package.json index 19f3f37..14fc031 100644 --- a/codemods/jssg/replace-string-ref/package.json +++ b/codemods/jssg/replace-string-ref/package.json @@ -1,5 +1,5 @@ { - "name": "@react/replace-string-ref", + "name": "@react-new/replace-string-ref", "version": "0.1.0", "description": "Replace string refs with callback refs in React class components", "type": "module", diff --git a/codemods/jssg/replace-use-form-state/README.md b/codemods/jssg/replace-use-form-state/README.md index b6fff1f..4d2a790 100644 --- a/codemods/jssg/replace-use-form-state/README.md +++ b/codemods/jssg/replace-use-form-state/README.md @@ -2,12 +2,10 @@ Rename `useFormState` to `useActionState` in `react-dom` imports and usages. -## Run Locally - -Run this command from this directory: +## Usage ```bash -pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +npx codemod @react-new/replace-use-form-state --target ``` ## Development diff --git a/codemods/jssg/replace-use-form-state/codemod.yaml b/codemods/jssg/replace-use-form-state/codemod.yaml index 3b2eea4..b648f6b 100644 --- a/codemods/jssg/replace-use-form-state/codemod.yaml +++ b/codemods/jssg/replace-use-form-state/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" -name: "@react/replace-use-form-state" +name: "@react-new/replace-use-form-state" version: "0.1.0" description: "Rename useFormState to useActionState in react-dom imports and usages" author: "Codemod " @@ -14,6 +14,6 @@ keywords: ["React"] registry: access: "public" - visibility: "public" + visibility: "private" capabilities: [] diff --git a/codemods/jssg/replace-use-form-state/package.json b/codemods/jssg/replace-use-form-state/package.json index 34da452..fbcf347 100644 --- a/codemods/jssg/replace-use-form-state/package.json +++ b/codemods/jssg/replace-use-form-state/package.json @@ -1,5 +1,5 @@ { - "name": "@react/replace-use-form-state", + "name": "@react-new/replace-use-form-state", "version": "0.1.0", "description": "Rename useFormState to useActionState in react-dom imports and usages", "type": "module", diff --git a/codemods/jssg/use-context-hook/README.md b/codemods/jssg/use-context-hook/README.md index 7a7ed78..c87cf70 100644 --- a/codemods/jssg/use-context-hook/README.md +++ b/codemods/jssg/use-context-hook/README.md @@ -2,12 +2,10 @@ Transform `React.useContext()` and `useContext()` calls into `use()`. -## Run Locally - -Run this command from this directory: +## Usage ```bash -pnpm dlx codemod@latest jssg run --language tsx --target ./scripts/codemod.ts +npx codemod @react-new/use-context-hook --target ``` ## Development diff --git a/codemods/jssg/use-context-hook/codemod.yaml b/codemods/jssg/use-context-hook/codemod.yaml index f23fedd..1a161a4 100644 --- a/codemods/jssg/use-context-hook/codemod.yaml +++ b/codemods/jssg/use-context-hook/codemod.yaml @@ -1,6 +1,6 @@ schema_version: "1.0" -name: "@react/use-context-hook" +name: "@react-new/use-context-hook" version: "0.1.0" description: "Transform React.useContext() and useContext() to use() hook" author: "Codemod " @@ -15,6 +15,6 @@ keywords: ["React"] registry: access: "public" - visibility: "public" + visibility: "private" capabilities: [] diff --git a/codemods/jssg/use-context-hook/package.json b/codemods/jssg/use-context-hook/package.json index 28b51bf..fb190e8 100644 --- a/codemods/jssg/use-context-hook/package.json +++ b/codemods/jssg/use-context-hook/package.json @@ -1,5 +1,5 @@ { - "name": "@react/use-context-hook", + "name": "@react-new/use-context-hook", "version": "0.1.0", "description": "Transform React.useContext() and useContext() to use() hook", "type": "module", diff --git a/package.json b/package.json index a439355..75610b2 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "scripts": { "lint": "node ./scripts/validate-docs.mjs", "test": "pnpm run test:active", - "test:active": "pnpm -r --filter \"@react/*\" test", + "test:active": "pnpm -r --filter \"@react-new/*\" test", "check-types": "pnpm run check-types:active", - "check-types:active": "pnpm -r --filter \"@react/*\" check-types", + "check-types:active": "pnpm -r --filter \"@react-new/*\" check-types", "test:legacy": "bash -lc 'cd codemods/legacy && pnpm install --ignore-workspace --frozen-lockfile --force && pnpm run test:ci'", "ci": "pnpm run lint && pnpm run test:active && pnpm run check-types:active && pnpm run test:legacy" }, diff --git a/scripts/validate-docs.mjs b/scripts/validate-docs.mjs index 29805a6..cc35678 100644 --- a/scripts/validate-docs.mjs +++ b/scripts/validate-docs.mjs @@ -6,8 +6,8 @@ const rootReadme = fs.readFileSync(path.join(repoRoot, "README.md"), "utf8"); const legacyReadme = fs.readFileSync(path.join(repoRoot, "LEGACY.md"), "utf8"); const forbiddenRootPatterns = [ - { label: "Codemod Registry command claim", regex: /npx codemod react\// }, - { label: "package-style codemod run claim", regex: /codemod run @react\// }, + { label: "old registry command claim", regex: /npx codemod react\// }, + { label: "old package-style codemod run claim", regex: /codemod run @react\// }, ]; for (const pattern of forbiddenRootPatterns) { @@ -26,7 +26,7 @@ for (const name of fs.readdirSync(transformRoot)) { const contents = fs.readFileSync(readmePath, "utf8"); if (/codemod run @react\//.test(contents) || /npx codemod react\//.test(contents)) { - throw new Error(`${path.relative(repoRoot, readmePath)} contains a forbidden published-run claim.`); + throw new Error(`${path.relative(repoRoot, readmePath)} contains a forbidden old-scope run claim.`); } } From ca4c98ecb25ee61960f3f9b0d84bac6ad2b9e040 Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Wed, 15 Apr 2026 21:32:33 +0200 Subject: [PATCH 25/29] setup changesets --- .changeset/README.md | 11 + .changeset/config.json | 15 + .github/workflows/release.yml | 70 +++ CONTRIBUTING.md | 60 +++ package.json | 3 + pnpm-lock.yaml | 793 +++++++++++++++++++++++++++++++ scripts/sync-codemod-versions.sh | 20 + scripts/tag-and-publish.sh | 28 ++ 8 files changed, 1000 insertions(+) create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .github/workflows/release.yml create mode 100644 CONTRIBUTING.md create mode 100755 scripts/sync-codemod-versions.sh create mode 100755 scripts/tag-and-publish.sh diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..6b45e55 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,11 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in the readme](https://github.com/changesets/changesets/blob/main/packages/changesets-cli/README.md) + +It is used by the repository to track versioning and release codemods. + +## Adding a changeset + +Run `pnpm changeset` to create a new changeset for your changes. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..43b13d3 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [], + "privatePackages": { + "version": false, + "tag": false + } +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4709a47 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +name: Release + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +permissions: + id-token: write + contents: write + pull-requests: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + outputs: + published: ${{ steps.changesets.outputs.published }} + changed_dirs: ${{ steps.tag.outputs.changed_dirs }} + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Create Release PR or Publish + id: changesets + uses: changesets/action@v1 + with: + version: pnpm version-packages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Tag released versions + if: steps.changesets.outputs.hasChangesets == 'false' + id: tag + run: bash scripts/tag-and-publish.sh + + publish: + name: Publish ${{ matrix.dir }} + needs: release + if: needs.release.outputs.changed_dirs != '[]' && needs.release.outputs.changed_dirs != '' + runs-on: ubuntu-latest + strategy: + matrix: + dir: ${{ fromJson(needs.release.outputs.changed_dirs) }} + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Publish codemod + uses: codemod/publish-action@v1 + with: + path: ${{ matrix.dir }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0e6af29 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,60 @@ +# Contributing + +Thanks for your interest in contributing to react-codemod! + +## Development setup + +```bash +# Install dependencies +pnpm install + +# Run all tests +pnpm test + +# Type-check all codemods +pnpm check-types +``` + +## Making changes + +1. Create a branch from `main`. +2. Make your changes and add or update tests. +3. Run `pnpm test` and `pnpm check-types` to verify everything passes. +4. Add a changeset (see below). +5. Open a pull request. + +## Adding a changeset + +This repo uses [Changesets](https://github.com/changesets/changesets) for versioning and releases. Every PR that changes a codemod must include a changeset. + +```bash +pnpm changeset +``` + +Follow the prompts to: +1. Select the affected codemod(s). +2. Choose the semver bump type — **patch** for bug fixes, **minor** for new features, **major** for breaking changes. +3. Write a short summary of the change. + +This creates a markdown file in `.changeset/` that should be committed with your PR. + +## Release workflow + +1. Merge a PR with one or more changesets into `main`. +2. CI automatically opens a **Version Packages** PR that bumps versions in `package.json` and `codemod.yaml`. +3. Merge the version PR — git tags are created and the updated codemods are published to the Codemod registry. + +## Adding a new codemod + +Each codemod lives in its own directory under `codemods/jssg/`: + +``` +codemods/jssg// + scripts/codemod.ts # Codemod logic (jssg / ast-grep) + tests/ # Input/expected test fixtures + codemod.yaml # Codemod manifest + workflow.yaml # Execution workflow + package.json +``` + +Use an existing codemod as a reference when creating a new one. diff --git a/package.json b/package.json index 75610b2..106703f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "check-types": "pnpm run check-types:active", "check-types:active": "pnpm -r --filter \"@react-new/*\" check-types", "test:legacy": "bash -lc 'cd codemods/legacy && pnpm install --ignore-workspace --frozen-lockfile --force && pnpm run test:ci'", + "changeset": "changeset", + "version-packages": "changeset version && bash scripts/sync-codemod-versions.sh", "ci": "pnpm run lint && pnpm run test:active && pnpm run check-types:active && pnpm run test:legacy" }, "keywords": [ @@ -26,6 +28,7 @@ }, "packageManager": "pnpm@9.14.2", "devDependencies": { + "@changesets/cli": "^2.30.0", "@types/node": "latest" }, "dependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92bc53d..27ccfc0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ importers: specifier: ^0.0.2 version: 0.0.2 devDependencies: + '@changesets/cli': + specifier: ^2.30.0 + version: 2.30.0(@types/node@25.6.0) '@types/node': specifier: latest version: 25.6.0 @@ -116,15 +119,377 @@ importers: packages: + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@changesets/apply-release-plan@7.1.0': + resolution: {integrity: sha512-yq8ML3YS7koKQ/9bk1PqO0HMzApIFNwjlwCnwFEXMzNe8NpzeeYYKCmnhWJGkN8g7E51MnWaSbqRcTcdIxUgnQ==} + + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.30.0': + resolution: {integrity: sha512-5D3Nk2JPqMI1wK25pEymeWRSlSMdo5QOGlyfrKg0AOufrUcjEE3RQgaCpHoBiM31CSNrtSgdJ0U6zL1rLDDfBA==} + hasBin: true + + '@changesets/config@3.1.3': + resolution: {integrity: sha512-vnXjcey8YgBn2L1OPWd3ORs0bGC4LoYcK/ubpgvzNVr53JXV5GiTVj7fWdMRsoKUH7hhhMAQnsJUqLr21EncNw==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-release-plan@4.0.15': + resolution: {integrity: sha512-Q04ZaRPuEVZtA+auOYgFaVQQSA98dXiVe/yFaZfY7hoSmQICHGvP0TF4u3EDNHWmmCS4ekA/XSpKlSM2PyTS2g==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.3': + resolution: {integrity: sha512-ZDmNc53+dXdWEv7fqIUSgRQOLYoUom5Z40gmLgmATmYR9NbL6FJJHwakcCpzaeCy+1D0m0n7mT4jj2B/MQPl7A==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.7': + resolution: {integrity: sha512-D1G4AUYGrBEk8vj8MGwf75k9GpN6XL3wg8i42P2jZZwFLXnlr2Pn7r9yuQNbaMCarP7ZQWNJbV6XLeysAIMhTA==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@codemod.com/jssg-types@1.5.1': resolution: {integrity: sha512-pCngSoz7hpNiMMkoVi+b4uqy7cA2XuJqEaOx52KYN/25bOdzJ7IqU9DEMukOKnWyFt9iy3UBinayit1P1+9wXA==} + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + '@jssg/utils@0.0.2': resolution: {integrity: sha512-eCQv5Xs9yfI6OKq2PQ8SyKsxPhdiaJY/XAFJmsZiVbsK5DIBBmGBA95CsyD4JFWXDKkNv6Q7ER/Kbp6/uWzB2w==} + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + better-path-resolve@1.0.0: + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + detect-indent@6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + enquirer@2.4.1: + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + extendable-error@0.1.7: + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + human-id@4.1.3: + resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} + hasBin: true + + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-subdir@1.2.0: + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} + + is-windows@1.0.2: + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash.startcase@4.4.0: + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + outdent@0.5.0: + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} + + p-filter@2.1.0: + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-map@2.1.0: + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + read-yaml-file@1.1.0: + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + spawndamnit@3.0.1: + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + term-size@2.2.1: + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + typescript@6.0.2: resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==} engines: {node: '>=14.17'} @@ -133,16 +498,444 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + snapshots: + '@babel/runtime@7.29.2': {} + + '@changesets/apply-release-plan@7.1.0': + dependencies: + '@changesets/config': 3.1.3 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + detect-indent: 6.1.0 + fs-extra: 7.0.1 + lodash.startcase: 4.4.0 + outdent: 0.5.0 + prettier: 2.8.8 + resolve-from: 5.0.0 + semver: 7.7.4 + + '@changesets/assemble-release-plan@6.0.9': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + semver: 7.7.4 + + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.30.0(@types/node@25.6.0)': + dependencies: + '@changesets/apply-release-plan': 7.1.0 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.3 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.15 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.3(@types/node@25.6.0) + '@manypkg/get-packages': 1.1.3 + ansi-colors: 4.1.3 + enquirer: 2.4.1 + fs-extra: 7.0.1 + mri: 1.2.0 + package-manager-detector: 0.2.11 + picocolors: 1.1.1 + resolve-from: 5.0.0 + semver: 7.7.4 + spawndamnit: 3.0.1 + term-size: 2.2.1 + transitivePeerDependencies: + - '@types/node' + + '@changesets/config@3.1.3': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + micromatch: 4.0.8 + + '@changesets/errors@0.2.0': + dependencies: + extendable-error: 0.1.7 + + '@changesets/get-dependents-graph@2.1.3': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + picocolors: 1.1.1 + semver: 7.7.4 + + '@changesets/get-release-plan@4.0.15': + dependencies: + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.3 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.7 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/get-version-range-type@0.4.0': {} + + '@changesets/git@3.0.4': + dependencies: + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 + is-subdir: 1.2.0 + micromatch: 4.0.8 + spawndamnit: 3.0.1 + + '@changesets/logger@0.1.1': + dependencies: + picocolors: 1.1.1 + + '@changesets/parse@0.4.3': + dependencies: + '@changesets/types': 6.1.0 + js-yaml: 4.1.1 + + '@changesets/pre@2.0.2': + dependencies: + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + fs-extra: 7.0.1 + + '@changesets/read@0.6.7': + dependencies: + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.3 + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + p-filter: 2.1.0 + picocolors: 1.1.1 + + '@changesets/should-skip-package@0.1.2': + dependencies: + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 + + '@changesets/types@4.1.0': {} + + '@changesets/types@6.1.0': {} + + '@changesets/write@0.4.0': + dependencies: + '@changesets/types': 6.1.0 + fs-extra: 7.0.1 + human-id: 4.1.3 + prettier: 2.8.8 + '@codemod.com/jssg-types@1.5.1': {} + '@inquirer/external-editor@1.0.3(@types/node@25.6.0)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.2 + optionalDependencies: + '@types/node': 25.6.0 + '@jssg/utils@0.0.2': {} + '@manypkg/find-root@1.1.0': + dependencies: + '@babel/runtime': 7.29.2 + '@types/node': 12.20.55 + find-up: 4.1.0 + fs-extra: 8.1.0 + + '@manypkg/get-packages@1.1.3': + dependencies: + '@babel/runtime': 7.29.2 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 + fs-extra: 8.1.0 + globby: 11.1.0 + read-yaml-file: 1.1.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@types/node@12.20.55': {} + '@types/node@25.6.0': dependencies: undici-types: 7.19.2 + ansi-colors@4.1.3: {} + + ansi-regex@5.0.1: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-union@2.1.0: {} + + better-path-resolve@1.0.0: + dependencies: + is-windows: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + chardet@2.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + detect-indent@6.1.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + enquirer@2.4.1: + dependencies: + ansi-colors: 4.1.3 + strip-ansi: 6.0.1 + + esprima@4.0.1: {} + + extendable-error@0.1.7: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + graceful-fs@4.2.11: {} + + human-id@4.1.3: {} + + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + is-subdir@1.2.0: + dependencies: + better-path-resolve: 1.0.0 + + is-windows@1.0.2: {} + + isexe@2.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash.startcase@4.4.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mri@1.2.0: {} + + outdent@0.5.0: {} + + p-filter@2.1.0: + dependencies: + p-map: 2.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-map@2.1.0: {} + + p-try@2.2.0: {} + + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.11 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-type@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + pify@4.0.1: {} + + prettier@2.8.8: {} + + quansync@0.2.11: {} + + queue-microtask@1.2.3: {} + + read-yaml-file@1.1.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.2 + pify: 4.0.1 + strip-bom: 3.0.0 + + resolve-from@5.0.0: {} + + reusify@1.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safer-buffer@2.1.2: {} + + semver@7.7.4: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + spawndamnit@3.0.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + sprintf-js@1.0.3: {} + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom@3.0.0: {} + + term-size@2.2.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + typescript@6.0.2: {} undici-types@7.19.2: {} + + universalify@0.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 diff --git a/scripts/sync-codemod-versions.sh b/scripts/sync-codemod-versions.sh new file mode 100755 index 0000000..fd0b363 --- /dev/null +++ b/scripts/sync-codemod-versions.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Syncs the version field in each codemod's codemod.yaml with its package.json. +# Run after `changeset version` to keep both files in sync. + +set -euo pipefail + +for pkg_json in codemods/jssg/*/package.json; do + dir="$(dirname "$pkg_json")" + codemod_yaml="$dir/codemod.yaml" + + if [ ! -f "$codemod_yaml" ]; then + continue + fi + + version="$(node -p "require('./$pkg_json').version")" + # Replace the version line in codemod.yaml + sed -i'' -e "s/^version: .*/version: \"$version\"/" "$codemod_yaml" + + echo "Synced $codemod_yaml to version $version" +done diff --git a/scripts/tag-and-publish.sh b/scripts/tag-and-publish.sh new file mode 100755 index 0000000..46cdbe8 --- /dev/null +++ b/scripts/tag-and-publish.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Creates git tags for each codemod whose version was bumped by changesets. +# Outputs the list of changed codemod directories for the publish job. +# Tags follow the pattern: @v + +set -euo pipefail + +changed_dirs="[]" + +for pkg_json in codemods/jssg/*/package.json; do + dir="$(dirname "$pkg_json")" + name="$(node -p "require('./$pkg_json').name")" + version="$(node -p "require('./$pkg_json').version")" + tag="${name}@v${version}" + + if git rev-parse "$tag" >/dev/null 2>&1; then + echo "Tag $tag already exists, skipping" + continue + fi + + echo "Creating tag $tag" + git tag "$tag" + changed_dirs="$(echo "$changed_dirs" | node -p "JSON.stringify([...JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')), \"$dir\"])")" +done + +git push --tags + +echo "changed_dirs=$changed_dirs" >> "$GITHUB_OUTPUT" From 03ea4869e2bc116686167293b133fe12171048ed Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Thu, 16 Apr 2026 13:54:48 +0200 Subject: [PATCH 26/29] fix regressions --- TESTING_PLAN.md | 197 ++++++++++++++ TESTING_REPORT.md | 245 ++++++++++++++++++ .../scripts/codemod.ts | 134 ++++++++-- .../tests/multiline-jsx/expected.tsx | 13 + .../tests/multiline-jsx/input.tsx | 12 + .../tests/multiline-jsx/metrics.json | 11 + .../tests/render-and-unmount/expected.tsx | 12 + .../tests/render-and-unmount/input.tsx | 9 + .../tests/render-and-unmount/metrics.json | 18 ++ .../tests/unmount-member/expected.tsx | 7 + .../tests/unmount-member/input.tsx | 5 + .../tests/unmount-member/metrics.json | 11 + .../tests/unmount-named/expected.tsx | 7 + .../tests/unmount-named/input.tsx | 5 + .../tests/unmount-named/metrics.json | 11 + .../replace-use-form-state/scripts/codemod.ts | 128 +++++++-- .../tests/aliased-to-same-name/expected.tsx | 2 +- .../tests/default-and-named/expected.tsx | 5 +- .../tests/default-import/expected.tsx | 3 +- .../expected.tsx | 4 +- .../expected.tsx | 2 +- .../tests/named-import-2/expected.tsx | 3 +- .../tests/named-import-3/expected.tsx | 3 +- .../tests/named-import/expected.tsx | 2 +- .../tests/namespace-and-named/expected.tsx | 4 +- .../tests/shadowed-local-binding/expected.tsx | 2 +- .../tests/type-identifier-rename/expected.tsx | 2 +- .../tests/wildcard-import/expected.tsx | 3 +- 28 files changed, 802 insertions(+), 58 deletions(-) create mode 100644 TESTING_PLAN.md create mode 100644 TESTING_REPORT.md create mode 100644 codemods/jssg/replace-reactdom-render/tests/multiline-jsx/expected.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/multiline-jsx/input.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/multiline-jsx/metrics.json create mode 100644 codemods/jssg/replace-reactdom-render/tests/render-and-unmount/expected.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/render-and-unmount/input.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/render-and-unmount/metrics.json create mode 100644 codemods/jssg/replace-reactdom-render/tests/unmount-member/expected.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/unmount-member/input.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/unmount-member/metrics.json create mode 100644 codemods/jssg/replace-reactdom-render/tests/unmount-named/expected.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/unmount-named/input.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/unmount-named/metrics.json diff --git a/TESTING_PLAN.md b/TESTING_PLAN.md new file mode 100644 index 0000000..c46cf7f --- /dev/null +++ b/TESTING_PLAN.md @@ -0,0 +1,197 @@ +# Side-by-Side Codemod Testing Plan + +> Testing `@react-new/*` JSSG codemods vs `reactjs/react-codemod` jscodeshift counterparts on real-world open-source repos. + +## Executive Summary + +We searched public GitHub repos for the exact code patterns each of our 6 JSSG codemods targets. Below are the recommended test repos, organized by codemod, with match counts and React version context. + +**Key finding:** `useFormState` from `react-dom` has **zero real-world adoption**. No open-source repo imports `useFormState` from `react-dom`. The API was renamed to `useActionState` before it saw meaningful use. + +--- + +## Recommended Test Repos + +### Tier 1 — Multi-Pattern Repos (highest value) + +| Repo | Stars | React Version | Patterns Found | Language | +|------|-------|---------------|----------------|----------| +| **[youzan/zent](https://github.com/youzan/zent)** | 2.2k | 17.0.x (dev), ^17 (peer) | `ReactDOM.render` (~398), `react-dom/test-utils` (~17), `useContext` | TypeScript 65% | +| **[salesforce/design-system-react](https://github.com/salesforce/design-system-react)** | ~1k | ^17.0.2 (dev), >=16.8 (peer) | `ReactDOM.render` (~315), `useContext` | JavaScript | +| **[atlassian/react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)** | 33k | 16.13.1 (dev), ^16.8.5‖^17‖^18 (peer) | `react-dom/test-utils` (multiple), `ReactDOM.render` | JS + Flow | + +### Tier 2 — Single-Pattern Repos (well-known, good signal) + +| Repo | React Version | Primary Pattern | Match Count | +|------|---------------|-----------------|-------------| +| **[MetaMask/metamask-extension](https://github.com/MetaMask/metamask-extension)** | 13.1k stars, React ^16.12.0 | `react-dom/test-utils` | ~18 | +| **[rsuite/rsuite](https://github.com/rsuite/rsuite)** | React ^19.0.0 (dev), >=18 (peer) | `ReactDOM.render` | ~776 | +| **[OnsenUI/OnsenUI](https://github.com/OnsenUI/OnsenUI)** (react-onsenui) | React ^18 (peer) | `react-dom/test-utils` | ~37 | + +### Tier 3 — Legacy Pattern Repos (older code, needed for PropTypes + string refs) + +| Repo | React Version | Patterns Found | Notes | +|------|---------------|----------------|-------| +| **[nylas/nylas-mail](https://github.com/nylas/nylas-mail)** | 24.8k stars, React ~15.x (Electron) | `React.PropTypes` (~193), `this.refs.` (~41) | Archived 2017, JS + CoffeeScript | +| **[azat-co/react-quickly](https://github.com/azat-co/react-quickly)** | React ~15.x | `React.PropTypes` (~155), `this.refs.` (~120) | Book examples, highest string ref count | +| **[cockpit-project/cockpit](https://github.com/cockpit-project/cockpit)** | 11k stars, varies | `this.refs.` (~54) | Active project, Linux admin tool | + +--- + +## Per-Codemod Breakdown + +### 1. `replace-reactdom-render` — ReactDOM.render → createRoot + +**Pattern:** `ReactDOM.render(element, container)` → `createRoot(container).render(element)` + +| Repo | Matches | Notes | +|------|---------|-------| +| rsuite/rsuite | ~776 | Largest count, but already on React 19 dev | +| alibaba-fusion/next | ~657 | Alibaba component library | +| kingdee/kdesign | ~482 | React component design system | +| **youzan/zent** | ~398 | ★ React 17, TypeScript, ideal target | +| **salesforce/design-system-react** | ~315 | ★ React 17, enterprise library | +| nfl/react-helmet | ~100+ | Widely used, small scope | + +**Recommended test repo:** `youzan/zent` (React 17 + TypeScript + high match count) + +**Legacy counterpart:** `npx react-codemod react/19/replace-reactdom-render` + +--- + +### 2. `replace-act-import` — react-dom/test-utils → react + +**Pattern:** `import { act } from 'react-dom/test-utils'` → `import { act } from 'react'` + +| Repo | Matches | Notes | +|------|---------|-------| +| **OnsenUI/OnsenUI** | ~37 | ★ Highest count | +| Foundry376/Mailspring | ~23 | Electron email client | +| **MetaMask/metamask-extension** | ~18 | ★ Very well-known, React 16 | +| youzan/zent | ~17 | Also has ReactDOM.render | +| atlassian/react-beautiful-dnd | multiple | Popular DnD library | + +**Recommended test repos:** `MetaMask/metamask-extension` (brand recognition) + `OnsenUI/OnsenUI` (highest count) + +**Legacy counterpart:** `npx react-codemod react/19/replace-act-import` + +--- + +### 3. `react-proptypes-to-prop-types` — React.PropTypes → prop-types package + +**Pattern:** `React.PropTypes.xxx` → `import PropTypes from 'prop-types'; PropTypes.xxx` + +| Repo | Matches | Notes | +|------|---------|-------| +| jianliaoim/talk-os | ~253 | CoffeeScript + React, older | +| **nylas/nylas-mail** | ~193 | ★ Archived, also has string refs | +| **azat-co/react-quickly** | ~155 | ★ Book examples, also has string refs | +| yhat/rodeo | ~125 | Data science IDE, older | + +**Note:** This pattern only exists in React ≤15.x codebases. All recommended repos are older/archived. This is expected — React.PropTypes was removed in React 16. + +**Recommended test repo:** `nylas/nylas-mail` (well-known + has 2 patterns) + +**Legacy counterpart:** `npx react-codemod React-PropTypes-to-prop-types` + +--- + +### 4. `replace-string-ref` — ref="string" → callback ref + +**Pattern:** `ref="myRef"` in JSX inside class components → `ref={(ref) => { this.myRef = ref; }}` + +| Repo | Matches | Notes | +|------|---------|-------| +| **azat-co/react-quickly** | ~120 | ★ Highest count, also has PropTypes | +| cockpit-project/cockpit | ~54 | Active Linux admin tool | +| nicehash/whistle | ~52 | Network debugging tool | +| bitshares/bitshares-ui | ~43 | Crypto exchange UI | +| **nylas/nylas-mail** | ~41 | Also has PropTypes | +| BoostIO/BoostNote-Legacy | many | Note-taking app | +| atom/atom | ~36 | Archived text editor | + +**Note:** Like PropTypes, string refs only exist in older codebases (React ≤15.x, deprecated from 16). + +**Recommended test repo:** `azat-co/react-quickly` (highest count + PropTypes combo) + +**Legacy counterpart:** `npx react-codemod react/19/replace-string-ref` + +--- + +### 5. `use-context-hook` — useContext → use + +**Pattern:** `useContext(SomeContext)` / `React.useContext(SomeContext)` → `use(SomeContext)` + +This pattern is **ubiquitous** — virtually every React application uses `useContext`. Any of the Tier 1 repos will work. + +**Recommended test repos:** `youzan/zent` + `salesforce/design-system-react` (already testing other patterns) + +**Legacy counterpart:** `npx react-codemod react/19/use-context-hook` + +--- + +### 6. `replace-use-form-state` — useFormState → useActionState + +**Pattern:** `import { useFormState } from 'react-dom'` → `import { useActionState } from 'react'` + +**⚠️ ZERO real-world usage found.** + +Searched grep.app for `useFormState` combined with `react-dom` across all languages. Zero matches. The only `useFormState` in the wild comes from `react-hook-form` (completely different API). + +This API was renamed to `useActionState` before it saw meaningful public adoption. Testing should rely on **synthetic fixtures only** (which are already in our test suite). + +--- + +## Recommended Testing Matrix + +| Repo | render | act-import | PropTypes | string-ref | useContext | useFormState | +|------|--------|------------|-----------|------------|------------|--------------| +| youzan/zent | ✅ ~398 | ✅ ~17 | — | — | ✅ | — | +| salesforce/design-system-react | ✅ ~315 | — | — | — | ✅ | — | +| MetaMask/metamask-extension | — | ✅ ~18 | — | — | ✅ | — | +| nylas/nylas-mail | — | — | ✅ ~193 | ✅ ~41 | — | — | +| azat-co/react-quickly | — | — | ✅ ~155 | ✅ ~120 | — | — | +| *(synthetic fixtures)* | — | — | — | — | — | ✅ | + +**Minimum repos to cover all 5 viable codemods: 3** (zent + MetaMask + nylas-mail) + +--- + +## Test Execution Plan + +### For each codemod × repo pair: + +1. **Clone the repo** at a recent tagged release +2. **Run the JSSG codemod** (`@react-new/`): + ```bash + npx codemod@latest react-new/ --target + ``` +3. **Capture the diff** → `results/jssg//.diff` +4. **Reset the repo** (`git checkout .`) +5. **Run the legacy jscodeshift codemod** (from `reactjs/react-codemod`): + ```bash + npx react-codemod + ``` +6. **Capture the diff** → `results/legacy//.diff` +7. **Compare** the two diffs: + - Files transformed (count + list) + - Transformation correctness + - Edge cases handled differently + - Performance (execution time) +8. **Document** findings per codemod + +### Metrics to capture: +- **Coverage**: How many files / instances were transformed +- **Correctness**: Do both produce valid, equivalent code +- **Edge cases**: Where do they diverge +- **Performance**: Wall-clock execution time +- **Errors**: Any crashes or partial transformations + +--- + +## Notes + +- Match counts are approximate (from grep.app searches, may include comments/docs) +- Some repos are archived/unmaintained — that's fine for testing, the code patterns are what matter +- `useFormState` testing is synthetic-only; recommend documenting this proactively for the React team +- `useContext` is so widespread that any React 16.8+ repo works; no need for a dedicated test repo diff --git a/TESTING_REPORT.md b/TESTING_REPORT.md new file mode 100644 index 0000000..d5808dc --- /dev/null +++ b/TESTING_REPORT.md @@ -0,0 +1,245 @@ +# JSSG vs jscodeshift — Side-by-Side Testing Report + +> **Goal**: Determine whether the new JSSG (ast-grep) codemods regress any intentional behavior compared to the legacy jscodeshift codemods from `reactjs/react-codemod`. + +## Test Environment + +| Item | Detail | +|------|--------| +| **JSSG codemods** | `@react-new/*` (v0.1.0, published to Codemod Registry; regression fixes pending republish) | +| **Legacy codemods** | `react/19/*` (jscodeshift, from Codemod Registry) | +| **CLI** | `codemod@latest` with `--no-interactive` flag | +| **Test repos** | youzan/zent (React 17, TS), azat-co/react-quickly (React ~15, JS/JSX), atlassian/react-beautiful-dnd (React 16.13, JS+Flow) | + +--- + +## Summary + +| Codemod | Verdict | JSSG Files | Legacy Files | Notes | +|---------|---------|:----------:|:------------:|-------| +| `replace-reactdom-render` | **Perfect parity** | 4 | 4 | Fixed: now handles `unmountComponentAtNode` + correct indentation | +| `replace-act-import` | **JSSG wins** | **6** | 1 | JSSG transforms 6× more files | +| `use-context-hook` | **Perfect parity** | 47 | 47 | Byte-identical diffs | +| `replace-string-ref` | **JSSG wins** | **5** | 0 | Legacy skips `.jsx` files entirely | +| `replace-use-form-state` | **Perfect parity** | 1 | 1 | Fixed: now moves import from `react-dom` to `react` | +| `react-proptypes-to-prop-types` | No comparison | 2 | — | No legacy counterpart on registry | + +**Bottom line**: 0 regressions, 2 areas where JSSG outperforms, 3 perfect parity, 1 unverifiable. + +--- + +## Detailed Findings + +### 1. `replace-reactdom-render` — **Perfect Parity** (Fixed) + +**Repo**: youzan/zent (`packages/zent/src/`) + +Both codemods now produce equivalent output across all 4 files: + +| File | JSSG | Legacy | +|------|:----:|:------:| +| `dialog/open.tsx` | ✅ | ✅ | +| `preview-image/previewImage.tsx` | ✅ | ✅ | +| `notify/Notify.tsx` | ✅ | ✅ | +| `notice/Container.tsx` | ✅ | ✅ | + +**What was fixed**: +- Added `ReactDOM.unmountComponentAtNode(container)` → `createRoot(container).unmount()` pattern (member + named import) +- Fixed indentation bug caused by UTF-8 byte offset vs JS string index mismatch in files with non-ASCII characters (e.g. Chinese comments) +- Added multi-line JSX formatting: elements spanning multiple lines are placed on a new line inside `render()` with correct re-indentation + +Example transformation (Notify.tsx): +```diff +- ReactDOM.unmountComponentAtNode(container); ++ const root2 = createRoot(container); ++ root2.unmount(); +``` + +```diff +- ReactDOM.render( +- , +- container +- ); ++ const root1 = createRoot(container); ++ root1.render( ++ ++ ); +``` + +#### Recommendation + +No action needed — regressions resolved. + +--- + +### 2. `replace-act-import` — **JSSG Outperforms** + +**Repo**: atlassian/react-beautiful-dnd (`test/`) + +| Metric | JSSG | Legacy | +|--------|:----:|:------:| +| Files transformed | **6** | 1 | + +Both codemods produce the **same correct transformation** per file: + +```diff +-import { act } from 'react-dom/test-utils'; ++import { act } from "react"; +``` + +But the legacy codemod only picks up **1 of 6 files** (`touch-sensor/click-blocking.spec.js`), while JSSG correctly finds and transforms all 6. The legacy codemod appears to have a file-matching or traversal limitation that causes it to miss files in nested test directories. + +#### Recommendation + +No action needed — JSSG is strictly better here. + +--- + +### 3. `use-context-hook` — **Perfect Parity** + +**Repo**: youzan/zent (`packages/zent/src/`) + +| Metric | JSSG | Legacy | +|--------|:----:|:------:| +| Files transformed | 47 | 47 | +| Insertions | 104 | 104 | +| Deletions | 104 | 104 | +| Diff comparison | **Byte-identical** | — | + +Running `diff` on the two saved diffs produced **empty output**. Every file, every line, every character is identical between the two codemods. This is the gold standard result. + +Transformation pattern (applied consistently across all 47 files): + +```diff +-import { useContext } from 'react'; ++import { use } from 'react'; + ... +-const value = useContext(SomeContext); ++const value = use(SomeContext); +``` + +#### Recommendation + +No action needed — JSSG is at full parity. + +--- + +### 4. `replace-string-ref` — **JSSG Outperforms** + +**Repo**: azat-co/react-quickly (full repo) + +| Metric | JSSG | Legacy | +|--------|:----:|:------:| +| Files transformed | **5** | 0 | +| String refs replaced | **9** | 0 | + +The legacy codemod reports _"No changes were made during the codemod run"_ despite the repo containing numerous string refs in `.jsx` files. The legacy jscodeshift transform appears to have a **file extension filtering issue** — all target files are `.jsx`, and the legacy codemod seems to skip non-`.js`/`.ts` files. + +JSSG correctly transforms all instances: + +```diff +- ++ { this.refs.emailAddress = ref; }} type="text" /> +``` + +Files transformed by JSSG: `ch07/email/jsx/content.jsx`, `ch11/homework/jsx/content.jsx`, `ch12/email/email-webpack/jsx/content.jsx` (×2 refs), `ch12/email/email-webpack/source/content.jsx` (×2 refs), `ch17/message-board/source/app.jsx`. + +#### Recommendation + +No action needed — JSSG is strictly better here. + +--- + +### 5. `replace-use-form-state` — **Perfect Parity** (Fixed) + +**Repo**: synthetic fixture (no real-world repos use `useFormState` from `react-dom`) + +**Input**: +```tsx +import { useFormState } from "react-dom"; + +function Form() { + const [state, formAction] = useFormState(action, initialState); + return
    {state}; +} +``` + +| Aspect | JSSG Output | Legacy Output | +|--------|-------------|---------------| +| Rename | `useFormState` → `useActionState` ✅ | `useFormState` → `useActionState` ✅ | +| Import source | `import { useActionState } from "react"` ✅ | `import { useActionState } from "react"` ✅ | + +**What was fixed**: +- Named imports: now rewrites the import source from `"react-dom"` to `"react"` (or splits when other specifiers remain) +- Member access (`ReactDOM.useFormState`): now replaces the entire expression with `useActionState` and adds a `react` import +- Handles quote style preservation, aliases, default imports + named splits, and multiple react-dom import statements + +Example (mixed imports): +```diff +-import { createPortal, useFormState } from "react-dom"; +-import * as ReactDOM from "react-dom"; ++import { createPortal } from "react-dom"; ++import { useActionState } from "react"; ++import * as ReactDOM from "react-dom"; + ... +- const [state, formAction] = useFormState(increment, 0); ++ const [state, formAction] = useActionState(increment, 0); +- const otherState = ReactDOM.useFormState(increment, 0); ++ const otherState = useActionState(increment, 0); +``` + +#### Recommendation + +No action needed — regressions resolved. + +--- + +### 6. `react-proptypes-to-prop-types` — **No Legacy Comparison** + +**Repo**: azat-co/react-quickly (`ch13/`) + +The legacy counterpart (`React-PropTypes-to-prop-types`) is **not published on the Codemod Registry**, so no direct comparison is possible. + +The JSSG codemod works correctly on the 2 files tested: + +```diff ++const PropTypes = require('prop-types'); + ... +-React.PropTypes.object.isRequired ++PropTypes.object.isRequired +``` + +It intelligently uses `require()` syntax matching the file's existing module system (CommonJS). + +#### Recommendation + +Consider publishing the legacy transform to the registry to enable future comparison, or treat the JSSG version as the canonical implementation. + +--- + +## Action Items + +### Resolved Regressions + +All regressions found during initial testing have been fixed and retested. + +| # | Codemod | Issue | Resolution | +|---|---------|-------|------------| +| 1 | `replace-reactdom-render` | Missing `unmountComponentAtNode()` pattern | ✅ Added member + named import matching for `unmountComponentAtNode` → `createRoot().unmount()` | +| 2 | `replace-reactdom-render` | Indentation bugs (byte offset vs char index with non-ASCII) | ✅ Rewrote `getIndent()` to use line-based approach; added `reindentText()` for multi-line JSX | +| 3 | `replace-use-form-state` | Import source not changed from `react-dom` to `react` | ✅ Rewrote import handling: direct node replacement with source splitting, quote preservation, alias support | + +### Remaining + +| Codemod | Status | +|---------|--------| +| `replace-act-import` | No action needed — JSSG outperforms legacy (6× coverage) | +| `use-context-hook` | No action needed — perfect byte-identical parity | +| `replace-string-ref` | No action needed — JSSG outperforms legacy (handles `.jsx` files) | +| `react-proptypes-to-prop-types` | No action needed — works correctly; no legacy to compare against | diff --git a/codemods/jssg/replace-reactdom-render/scripts/codemod.ts b/codemods/jssg/replace-reactdom-render/scripts/codemod.ts index 5fa4bde..1d5d4bb 100644 --- a/codemods/jssg/replace-reactdom-render/scripts/codemod.ts +++ b/codemods/jssg/replace-reactdom-render/scripts/codemod.ts @@ -3,6 +3,20 @@ import type TSX from "codemod:ast-grep/langs/tsx"; import { useMetricAtom } from "codemod:metrics"; import { getImport } from "@jssg/utils/javascript/imports"; +function reindentText(text: string, fromCol: number, toCol: number): string { + if (fromCol === toCol) return text; + const lines = text.split("\n"); + const delta = toCol - fromCol; + return lines + .map((line, i) => { + if (i === 0) return line; + if (delta > 0) return " ".repeat(delta) + line; + const strip = Math.min(-delta, line.length - line.trimStart().length); + return line.slice(strip); + }) + .join("\n"); +} + function metricFile(filename: string): string { const cwd = process.cwd() + "/"; return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; @@ -39,15 +53,15 @@ function findReactDomMemberImportNames(rootNode: SgNode): Set): Set { +function findNamedImportNames(rootNode: SgNode, exportedName: string): Set { const names = new Set(); for (const importNode of rootNode.findAll({ rule: { kind: "import_statement" } })) { if (importSource(importNode) !== "react-dom") continue; for (const specifier of importNode.findAll({ rule: { kind: "import_specifier" } })) { - if (specifier.field("name")?.text() !== "render") continue; - names.add(specifier.field("alias")?.text() ?? "render"); + if (specifier.field("name")?.text() !== exportedName) continue; + names.add(specifier.field("alias")?.text() ?? exportedName); } } @@ -61,9 +75,10 @@ const transform: Transform = async (root) => { const transformMetric = useMetricAtom("reactdom-render-replacements"); const reactDomMemberImportNames = findReactDomMemberImportNames(rootNode); - const reactDomRenderImportNames = findNamedRenderImportNames(rootNode); + const reactDomRenderImportNames = findNamedImportNames(rootNode, "render"); + const reactDomUnmountImportNames = findNamedImportNames(rootNode, "unmountComponentAtNode"); - if (reactDomMemberImportNames.size === 0 && reactDomRenderImportNames.size === 0) { + if (reactDomMemberImportNames.size === 0 && reactDomRenderImportNames.size === 0 && reactDomUnmountImportNames.size === 0) { return null; } @@ -77,6 +92,13 @@ const transform: Transform = async (root) => { }; const sourceCode = rootNode.text(); + const sourceLines = sourceCode.split("\n"); + + const getIndent = (statement: SgNode): string => { + const line = sourceLines[statement.range().start.line] ?? ""; + const match = line.match(/^(\s*)/); + return match ? match[1] : ""; + }; const applyRenderReplacement = (call: SgNode, pattern: string): void => { const args = call.field("arguments"); @@ -94,19 +116,47 @@ const transform: Transform = async (root) => { if (!statement) return; const rootName = nextRootName(); - const stmtStart = statement.range().start.index; - let indent = ""; - for (let i = stmtStart - 1; i >= 0; i--) { - const char = sourceCode[i]; - if (char === "\n") break; - if (char === " " || char === "\t") { - indent = char + indent; - } else { - indent = ""; - } + const indent = getIndent(statement); + const elementText = element.text(); + + let renderCall: string; + if (elementText.includes("\n")) { + const elementStartLine = sourceLines[element.range().start.line] ?? ""; + const originalCol = elementStartLine.length - elementStartLine.trimStart().length; + const desiredCol = indent.length + 2; + const reindented = reindentText(elementText, originalCol, desiredCol); + renderCall = `${rootName}.render(\n${" ".repeat(desiredCol)}${reindented}\n${indent})`; + } else { + renderCall = `${rootName}.render(${elementText})`; } - const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${rootName}.render(${element.text()});`; + const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${renderCall};`; + edits.push(statement.replace(replacement)); + hasTransformations = true; + transformMetric.increment({ + pattern, + file: metricFile(root.filename()), + }); + }; + + const applyUnmountReplacement = (call: SgNode, pattern: string): void => { + const args = call.field("arguments"); + if (!args) return; + + const argList = args.children().filter((child) => + child.kind() !== "(" && child.kind() !== ")" && child.kind() !== "," + ); + + if (argList.length < 1) return; + + const container = argList[0]!; + const statement = call.ancestors().find((ancestor) => ancestor.kind() === "expression_statement"); + if (!statement) return; + + const rootName = nextRootName(); + const indent = getIndent(statement); + + const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${rootName}.unmount();`; edits.push(statement.replace(replacement)); hasTransformations = true; transformMetric.increment({ @@ -148,6 +198,39 @@ const transform: Transform = async (root) => { if (!objectNode || !reactDomMemberImportNames.has(objectNode.text())) continue; applyRenderReplacement(call, "ReactDOM.render"); } + + const memberUnmountCalls = rootNode.findAll({ + rule: { + kind: "call_expression", + has: { + field: "function", + kind: "member_expression", + all: [ + { + has: { + field: "object", + kind: "identifier", + regex: ".*", + }, + }, + { + has: { + field: "property", + kind: "property_identifier", + regex: "^unmountComponentAtNode$", + }, + }, + ], + }, + }, + }); + + for (const call of memberUnmountCalls) { + const callee = call.field("function"); + const objectNode = callee?.field("object"); + if (!objectNode || !reactDomMemberImportNames.has(objectNode.text())) continue; + applyUnmountReplacement(call, "ReactDOM.unmountComponentAtNode"); + } } if (reactDomRenderImportNames.size > 0) { @@ -169,6 +252,25 @@ const transform: Transform = async (root) => { } } + if (reactDomUnmountImportNames.size > 0) { + const unmountCalls = rootNode.findAll({ + rule: { + kind: "call_expression", + has: { + field: "function", + kind: "identifier", + regex: ".*", + }, + }, + }); + + for (const call of unmountCalls) { + const callee = call.field("function"); + if (!callee || !reactDomUnmountImportNames.has(callee.text())) continue; + applyUnmountReplacement(call, "unmountComponentAtNode"); + } + } + if (!hasTransformations) { return null; } diff --git a/codemods/jssg/replace-reactdom-render/tests/multiline-jsx/expected.tsx b/codemods/jssg/replace-reactdom-render/tests/multiline-jsx/expected.tsx new file mode 100644 index 0000000..f0f2aa4 --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/multiline-jsx/expected.tsx @@ -0,0 +1,13 @@ +import { createRoot } from "react-dom/client"; +import * as ReactDOM from "react-dom"; + +function show(container) { + const root = createRoot(container); + root.render( + + ); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/multiline-jsx/input.tsx b/codemods/jssg/replace-reactdom-render/tests/multiline-jsx/input.tsx new file mode 100644 index 0000000..c831c91 --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/multiline-jsx/input.tsx @@ -0,0 +1,12 @@ +import * as ReactDOM from "react-dom"; + +function show(container) { + ReactDOM.render( + , + container + ); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/multiline-jsx/metrics.json b/codemods/jssg/replace-reactdom-render/tests/multiline-jsx/metrics.json new file mode 100644 index 0000000..6ee0fdc --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/multiline-jsx/metrics.json @@ -0,0 +1,11 @@ +{ + "reactdom-render-replacements": [ + { + "cardinality": { + "file": "tests/multiline-jsx/input.tsx", + "pattern": "ReactDOM.render" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/jssg/replace-reactdom-render/tests/render-and-unmount/expected.tsx b/codemods/jssg/replace-reactdom-render/tests/render-and-unmount/expected.tsx new file mode 100644 index 0000000..a1cde91 --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/render-and-unmount/expected.tsx @@ -0,0 +1,12 @@ +import { createRoot } from "react-dom/client"; +import * as ReactDOM from "react-dom"; + +function mount(container) { + const root = createRoot(container); + root.render(); +} + +function cleanup(container) { + const root1 = createRoot(container); + root1.unmount(); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/render-and-unmount/input.tsx b/codemods/jssg/replace-reactdom-render/tests/render-and-unmount/input.tsx new file mode 100644 index 0000000..de47b7e --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/render-and-unmount/input.tsx @@ -0,0 +1,9 @@ +import * as ReactDOM from "react-dom"; + +function mount(container) { + ReactDOM.render(, container); +} + +function cleanup(container) { + ReactDOM.unmountComponentAtNode(container); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/render-and-unmount/metrics.json b/codemods/jssg/replace-reactdom-render/tests/render-and-unmount/metrics.json new file mode 100644 index 0000000..707622b --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/render-and-unmount/metrics.json @@ -0,0 +1,18 @@ +{ + "reactdom-render-replacements": [ + { + "cardinality": { + "file": "tests/render-and-unmount/input.tsx", + "pattern": "ReactDOM.render" + }, + "count": 1 + }, + { + "cardinality": { + "file": "tests/render-and-unmount/input.tsx", + "pattern": "ReactDOM.unmountComponentAtNode" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/jssg/replace-reactdom-render/tests/unmount-member/expected.tsx b/codemods/jssg/replace-reactdom-render/tests/unmount-member/expected.tsx new file mode 100644 index 0000000..b32b914 --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/unmount-member/expected.tsx @@ -0,0 +1,7 @@ +import { createRoot } from "react-dom/client"; +import * as ReactDOM from "react-dom"; + +function cleanup(container) { + const root = createRoot(container); + root.unmount(); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/unmount-member/input.tsx b/codemods/jssg/replace-reactdom-render/tests/unmount-member/input.tsx new file mode 100644 index 0000000..95ce2fb --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/unmount-member/input.tsx @@ -0,0 +1,5 @@ +import * as ReactDOM from "react-dom"; + +function cleanup(container) { + ReactDOM.unmountComponentAtNode(container); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/unmount-member/metrics.json b/codemods/jssg/replace-reactdom-render/tests/unmount-member/metrics.json new file mode 100644 index 0000000..7cf3c4e --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/unmount-member/metrics.json @@ -0,0 +1,11 @@ +{ + "reactdom-render-replacements": [ + { + "cardinality": { + "file": "tests/unmount-member/input.tsx", + "pattern": "ReactDOM.unmountComponentAtNode" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/jssg/replace-reactdom-render/tests/unmount-named/expected.tsx b/codemods/jssg/replace-reactdom-render/tests/unmount-named/expected.tsx new file mode 100644 index 0000000..82a0078 --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/unmount-named/expected.tsx @@ -0,0 +1,7 @@ +import { createRoot } from "react-dom/client"; +import { unmountComponentAtNode } from "react-dom"; + +function cleanup(container) { + const root = createRoot(container); + root.unmount(); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/unmount-named/input.tsx b/codemods/jssg/replace-reactdom-render/tests/unmount-named/input.tsx new file mode 100644 index 0000000..083346c --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/unmount-named/input.tsx @@ -0,0 +1,5 @@ +import { unmountComponentAtNode } from "react-dom"; + +function cleanup(container) { + unmountComponentAtNode(container); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/unmount-named/metrics.json b/codemods/jssg/replace-reactdom-render/tests/unmount-named/metrics.json new file mode 100644 index 0000000..28c99ab --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/unmount-named/metrics.json @@ -0,0 +1,11 @@ +{ + "reactdom-render-replacements": [ + { + "cardinality": { + "file": "tests/unmount-named/input.tsx", + "pattern": "unmountComponentAtNode" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/jssg/replace-use-form-state/scripts/codemod.ts b/codemods/jssg/replace-use-form-state/scripts/codemod.ts index 79542ba..3b90d31 100644 --- a/codemods/jssg/replace-use-form-state/scripts/codemod.ts +++ b/codemods/jssg/replace-use-form-state/scripts/codemod.ts @@ -63,11 +63,7 @@ function findNamedUseFormStateImports(rootNode: SgNode): NamedUs return imports; } -function replacementImportSpecifierText(usedName: string): string { - return usedName === "useFormState" - ? "useActionState" - : `useActionState as ${usedName}`; -} +const REACT_MODULE = "react"; const transform: Transform = async (root) => { const rootNode = root.root(); @@ -77,6 +73,8 @@ const transform: Transform = async (root) => { const reactDOMMemberImportNames = findReactDOMMemberImportNames(rootNode); const namedUseFormStateImports = findNamedUseFormStateImports(rootNode); + let needsReactImport = false; + if (reactDOMMemberImportNames.size > 0) { const memberCalls = rootNode.findAll({ rule: { @@ -91,38 +89,84 @@ const transform: Transform = async (root) => { for (const member of memberCalls) { const objNode = member.field("object"); if (objNode && reactDOMMemberImportNames.has(objNode.text())) { - const propNode = member.field("property"); - if (propNode) { - edits.push(propNode.replace("useActionState")); - metric.increment({ file: metricFile(root.filename()), pattern: "member-call" }); - } + // Replace the entire member expression (ReactDOM.useFormState) with just useActionState + edits.push(member.replace("useActionState")); + needsReactImport = true; + metric.increment({ file: metricFile(root.filename()), pattern: "member-call" }); } } } if (namedUseFormStateImports.length > 0) { - let shouldRenameUnaliasedUses = false; + // Track import statements we've already edited to avoid duplicate edits + const handledImportRanges = new Set(); for (const entry of namedUseFormStateImports) { - edits.push(entry.specifier.replace(replacementImportSpecifierText(entry.localName))); - if (entry.localName === "useFormState") { - shouldRenameUnaliasedUses = true; + const rangeKey = `${entry.importNode.range().start.index}-${entry.importNode.range().end.index}`; + if (!handledImportRanges.has(rangeKey)) { + handledImportRanges.add(rangeKey); + + // Count other specifiers in this import (non-useFormState) + const allSpecifiers = entry.importNode.findAll({ rule: { kind: "import_specifier" } }); + const otherSpecifiers = allSpecifiers.filter( + (s) => s.field("name")?.text() !== "useFormState" + ); + const importClause = entry.importNode.find({ rule: { kind: "import_clause" } }); + const defaultIdentifier = importClause?.children().find((child) => child.kind() === "identifier"); + + // Detect the quote style from the original import source + const sourceNode = entry.importNode.field("source") ?? entry.importNode.find({ rule: { kind: "string" } }); + const quoteChar = sourceNode?.text().startsWith("'") ? "'" : '"'; + + const reactImportSpecifier = entry.localName !== "useFormState" + ? `useActionState as ${entry.localName}` + : "useActionState"; + + if (otherSpecifiers.length === 0 && !defaultIdentifier) { + // useFormState is the only specifier — replace entire import with react import + edits.push(entry.importNode.replace( + `import { ${reactImportSpecifier} } from ${quoteChar}react${quoteChar};` + )); + } else if (defaultIdentifier && otherSpecifiers.length === 0) { + // Has default import + useFormState only — remove named imports part, add react import + // e.g., import ReactDOM, { useFormState } from 'react-dom' + // → import ReactDOM from 'react-dom'; + // import { useActionState } from 'react'; + const defaultName = defaultIdentifier.text(); + edits.push(entry.importNode.replace( + `import ${defaultName} from ${quoteChar}${REACT_DOM_MODULE}${quoteChar};\nimport { ${reactImportSpecifier} } from ${quoteChar}react${quoteChar};` + )); + } else { + // Other specifiers remain — rebuild import without useFormState, add react import + const otherSpecTexts = otherSpecifiers.map((s) => { + const name = s.field("name")?.text(); + const alias = s.field("alias")?.text(); + return alias && alias !== name ? `${name} as ${alias}` : name; + }); + const prefix = defaultIdentifier ? `${defaultIdentifier.text()}, ` : ""; + edits.push(entry.importNode.replace( + `import ${prefix}{ ${otherSpecTexts.join(", ")} } from ${quoteChar}${REACT_DOM_MODULE}${quoteChar};\nimport { ${reactImportSpecifier} } from ${quoteChar}react${quoteChar};` + )); + } } - } - if (shouldRenameUnaliasedUses) { - const renameTargets = rootNode.findAll({ - rule: { - any: [ - { kind: "identifier", regex: "^useFormState$" }, - { kind: "type_identifier", regex: "^useFormState$" }, - ], - }, - }); + needsReactImport = true; - for (const node of renameTargets) { - if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) continue; - edits.push(node.replace("useActionState")); + if (entry.localName === "useFormState") { + // Rename all usage sites from useFormState to useActionState + const renameTargets = rootNode.findAll({ + rule: { + any: [ + { kind: "identifier", regex: "^useFormState$" }, + { kind: "type_identifier", regex: "^useFormState$" }, + ], + }, + }); + + for (const node of renameTargets) { + if (node.ancestors().some((ancestor) => ancestor.kind() === "import_statement")) continue; + edits.push(node.replace("useActionState")); + } } } @@ -130,6 +174,36 @@ const transform: Transform = async (root) => { } if (edits.length === 0) return null; + + // For member access cases (ReactDOM.useFormState), add import { useActionState } from "react" + // Named import cases are already handled above (import replacement includes the react import) + if (needsReactImport && namedUseFormStateImports.length === 0) { + const hasReactImport = getImport(rootNode, { + type: "named", + name: "useActionState", + from: REACT_MODULE, + }); + if (!hasReactImport) { + // Find last import statement to insert after + const allImports = rootNode.findAll({ rule: { kind: "import_statement" } }); + const lastImport = allImports[allImports.length - 1]; + if (lastImport) { + const endPos = lastImport.range().end.index; + edits.push({ + startPos: endPos, + endPos: endPos, + insertedText: '\nimport { useActionState } from "react";', + }); + } else { + edits.push({ + startPos: 0, + endPos: 0, + insertedText: 'import { useActionState } from "react";\n', + }); + } + } + } + return rootNode.commitEdits(edits); }; diff --git a/codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/expected.tsx b/codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/expected.tsx index 98785a6..968de3c 100644 --- a/codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/aliased-to-same-name/expected.tsx @@ -1,2 +1,2 @@ -import { useActionState } from 'react-dom'; +import { useActionState } from 'react'; function C(){ return useActionState(a,0); } diff --git a/codemods/jssg/replace-use-form-state/tests/default-and-named/expected.tsx b/codemods/jssg/replace-use-form-state/tests/default-and-named/expected.tsx index a37fafb..81949bc 100644 --- a/codemods/jssg/replace-use-form-state/tests/default-and-named/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/default-and-named/expected.tsx @@ -1,2 +1,3 @@ -import ReactDOM, { useActionState } from 'react-dom'; -function C(){ const x = useActionState(a,0); return ReactDOM.useActionState(b,1); } +import ReactDOM from 'react-dom'; +import { useActionState } from 'react'; +function C(){ const x = useActionState(a,0); return useActionState(b,1); } diff --git a/codemods/jssg/replace-use-form-state/tests/default-import/expected.tsx b/codemods/jssg/replace-use-form-state/tests/default-import/expected.tsx index 6fc2765..c359d05 100644 --- a/codemods/jssg/replace-use-form-state/tests/default-import/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/default-import/expected.tsx @@ -1,6 +1,7 @@ import ReactDOM from "react-dom"; +import { useActionState } from "react"; function StatefulForm({}) { - const [state, formAction] = ReactDOM.useActionState(increment, 0); + const [state, formAction] = useActionState(increment, 0); return
    ; } diff --git a/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx index 1d90ff0..2303201 100644 --- a/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-both-have-hook/expected.tsx @@ -1,3 +1,3 @@ -import { useActionState } from 'react-dom'; -import { useActionState as ufs } from 'react-dom'; +import { useActionState } from 'react'; +import { useActionState as ufs } from 'react'; function C(){ useActionState(a,0); return ufs(b,1); } diff --git a/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx index d17a3d8..2842302 100644 --- a/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/multiple-react-dom-imports-second-has-hook/expected.tsx @@ -1,3 +1,3 @@ import { createPortal } from 'react-dom'; -import { useActionState } from 'react-dom'; +import { useActionState } from 'react'; function C(){ return useActionState(a,0); } diff --git a/codemods/jssg/replace-use-form-state/tests/named-import-2/expected.tsx b/codemods/jssg/replace-use-form-state/tests/named-import-2/expected.tsx index 1fbd00d..e24620d 100644 --- a/codemods/jssg/replace-use-form-state/tests/named-import-2/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/named-import-2/expected.tsx @@ -1,4 +1,5 @@ -import { createPortal, useActionState } from "react-dom"; +import { createPortal } from "react-dom"; +import { useActionState } from "react"; function StatefulForm({}) { const [state, formAction] = useActionState(increment, 0); diff --git a/codemods/jssg/replace-use-form-state/tests/named-import-3/expected.tsx b/codemods/jssg/replace-use-form-state/tests/named-import-3/expected.tsx index c36827d..3915af5 100644 --- a/codemods/jssg/replace-use-form-state/tests/named-import-3/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/named-import-3/expected.tsx @@ -1,4 +1,5 @@ -import { useActionState as UFS, createPortal } from "react-dom"; +import { createPortal } from "react-dom"; +import { useActionState as UFS } from "react"; function StatefulForm({}) { const [state, formAction] = UFS(increment, 0); diff --git a/codemods/jssg/replace-use-form-state/tests/named-import/expected.tsx b/codemods/jssg/replace-use-form-state/tests/named-import/expected.tsx index 7d57506..02ae2e2 100644 --- a/codemods/jssg/replace-use-form-state/tests/named-import/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/named-import/expected.tsx @@ -1,4 +1,4 @@ -import { useActionState } from "react-dom"; +import { useActionState } from "react"; async function increment(previousState, formData) { return previousState + 1; diff --git a/codemods/jssg/replace-use-form-state/tests/namespace-and-named/expected.tsx b/codemods/jssg/replace-use-form-state/tests/namespace-and-named/expected.tsx index 7e7b98a..dc100c8 100644 --- a/codemods/jssg/replace-use-form-state/tests/namespace-and-named/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/namespace-and-named/expected.tsx @@ -1,3 +1,3 @@ import * as ReactDOM from 'react-dom'; -import { useActionState as useFormStateAlias } from 'react-dom'; -function C(){ const x = useFormStateAlias(a,0); return ReactDOM.useActionState(b,1); } +import { useActionState as useFormStateAlias } from 'react'; +function C(){ const x = useFormStateAlias(a,0); return useActionState(b,1); } diff --git a/codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/expected.tsx b/codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/expected.tsx index ab90583..fa7078c 100644 --- a/codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/shadowed-local-binding/expected.tsx @@ -1,2 +1,2 @@ -import { useActionState } from 'react-dom'; +import { useActionState } from 'react'; function C(){ const useActionState = make(); return useActionState(a,0); } diff --git a/codemods/jssg/replace-use-form-state/tests/type-identifier-rename/expected.tsx b/codemods/jssg/replace-use-form-state/tests/type-identifier-rename/expected.tsx index 5516ef0..d15e457 100644 --- a/codemods/jssg/replace-use-form-state/tests/type-identifier-rename/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/type-identifier-rename/expected.tsx @@ -1,3 +1,3 @@ -import { useActionState } from 'react-dom'; +import { useActionState } from 'react'; type useActionState = number; function C(){ return useActionState(a,0); } diff --git a/codemods/jssg/replace-use-form-state/tests/wildcard-import/expected.tsx b/codemods/jssg/replace-use-form-state/tests/wildcard-import/expected.tsx index cfcf7dc..78187b0 100644 --- a/codemods/jssg/replace-use-form-state/tests/wildcard-import/expected.tsx +++ b/codemods/jssg/replace-use-form-state/tests/wildcard-import/expected.tsx @@ -1,6 +1,7 @@ import * as ReactDOM from "react-dom"; +import { useActionState } from "react"; function StatefulForm({}) { - const [state, formAction] = ReactDOM.useActionState(increment, 0); + const [state, formAction] = useActionState(increment, 0); return
    ; } From 2e307f88175852d61630c0aa0a732268d6ecd61a Mon Sep 17 00:00:00 2001 From: Alex Bit Date: Thu, 16 Apr 2026 18:00:39 -0700 Subject: [PATCH 27/29] fix(replace-act-import): broaden workflow include/exclude globs Expand include patterns for spec files, __tests__, and tests/ trees so the workflow discovers typical test sources and JSSG fixtures. Add excludes for .git and JSSG expected golden files. Made-with: Cursor --- .../jssg/replace-act-import/workflow.yaml | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/codemods/jssg/replace-act-import/workflow.yaml b/codemods/jssg/replace-act-import/workflow.yaml index 363f8cc..4db52cd 100644 --- a/codemods/jssg/replace-act-import/workflow.yaml +++ b/codemods/jssg/replace-act-import/workflow.yaml @@ -11,3 +11,24 @@ nodes: js-ast-grep: js_file: scripts/codemod.ts language: "tsx" + # Test-oriented globs . + include: + - "**/*.test.ts" + - "**/*.test.tsx" + - "**/*.spec.ts" + - "**/*.spec.tsx" + - "**/__tests__/**/*.ts" + - "**/__tests__/**/*.tsx" + - "**/tests/**/*.ts" + - "**/tests/**/*.tsx" + exclude: + - "**/node_modules/**" + - "**/dist/**" + - "**/build/**" + - "**/.next/**" + - "**/out/**" + - "**/coverage/**" + - "**/.git/**" + # JSSG golden outputs (avoid touching when running workflow from package root) + - "**/tests/**/expected.ts" + - "**/tests/**/expected.tsx" From 507c11305fd5f7f0ee982770491e6a38e3899d6c Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Fri, 17 Apr 2026 18:39:06 +0200 Subject: [PATCH 28/29] test cal.com --- TESTING_PLAN.md | 14 ++++++++-- TESTING_REPORT.md | 66 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 71 insertions(+), 9 deletions(-) diff --git a/TESTING_PLAN.md b/TESTING_PLAN.md index c46cf7f..962d1e5 100644 --- a/TESTING_PLAN.md +++ b/TESTING_PLAN.md @@ -8,6 +8,8 @@ We searched public GitHub repos for the exact code patterns each of our 6 JSSG c **Key finding:** `useFormState` from `react-dom` has **zero real-world adoption**. No open-source repo imports `useFormState` from `react-dom`. The API was renamed to `useActionState` before it saw meaningful use. +**Additional modern spot-check:** As of 2026-04-17, `calcom/cal.com` redirects to `calcom/cal.diy`. It is a useful React 18/19 TypeScript monorepo target for validating `use-context-hook` and `replace-act-import` on current code. + --- ## Recommended Test Repos @@ -19,6 +21,7 @@ We searched public GitHub repos for the exact code patterns each of our 6 JSSG c | **[youzan/zent](https://github.com/youzan/zent)** | 2.2k | 17.0.x (dev), ^17 (peer) | `ReactDOM.render` (~398), `react-dom/test-utils` (~17), `useContext` | TypeScript 65% | | **[salesforce/design-system-react](https://github.com/salesforce/design-system-react)** | ~1k | ^17.0.2 (dev), >=16.8 (peer) | `ReactDOM.render` (~315), `useContext` | JavaScript | | **[atlassian/react-beautiful-dnd](https://github.com/atlassian/react-beautiful-dnd)** | 33k | 16.13.1 (dev), ^16.8.5‖^17‖^18 (peer) | `react-dom/test-utils` (multiple), `ReactDOM.render` | JS + Flow | +| **[calcom/cal.com](https://github.com/calcom/cal.com)** (redirects to [`calcom/cal.diy`](https://github.com/calcom/cal.diy)) | 41.4k | 18.2.0 in `apps/web`, `^18 || ^19` across workspace packages | `useContext` (~47 raw refs, 30 actual codemod hits), `react-dom/test-utils` (1) | TypeScript | ### Tier 2 — Single-Pattern Repos (well-known, good signal) @@ -70,8 +73,9 @@ We searched public GitHub repos for the exact code patterns each of our 6 JSSG c | **MetaMask/metamask-extension** | ~18 | ★ Very well-known, React 16 | | youzan/zent | ~17 | Also has ReactDOM.render | | atlassian/react-beautiful-dnd | multiple | Popular DnD library | +| **calcom/cal.com** | 1 | Modern React 18/19 monorepo, verified on tag `v6.2.0` | -**Recommended test repos:** `MetaMask/metamask-extension` (brand recognition) + `OnsenUI/OnsenUI` (highest count) +**Recommended test repos:** `MetaMask/metamask-extension` (brand recognition) + `OnsenUI/OnsenUI` (highest count) + `calcom/cal.com` (modern React 18/19 spot-check) **Legacy counterpart:** `npx react-codemod react/19/replace-act-import` @@ -124,7 +128,9 @@ We searched public GitHub repos for the exact code patterns each of our 6 JSSG c This pattern is **ubiquitous** — virtually every React application uses `useContext`. Any of the Tier 1 repos will work. -**Recommended test repos:** `youzan/zent` + `salesforce/design-system-react` (already testing other patterns) +**Additional verified modern repo:** `calcom/cal.com` (redirects to `calcom/cal.diy`) at tag `v6.2.0` yields 30 transformable files with the current local workflow. + +**Recommended test repos:** `youzan/zent` + `salesforce/design-system-react` + `calcom/cal.com` (modern React 18/19 monorepo) **Legacy counterpart:** `npx react-codemod react/19/use-context-hook` @@ -148,6 +154,7 @@ This API was renamed to `useActionState` before it saw meaningful public adoptio |------|--------|------------|-----------|------------|------------|--------------| | youzan/zent | ✅ ~398 | ✅ ~17 | — | — | ✅ | — | | salesforce/design-system-react | ✅ ~315 | — | — | — | ✅ | — | +| calcom/cal.com (`v6.2.0`) | — | ✅ 1 | — | — | ✅ 30 transformable files | — | | MetaMask/metamask-extension | — | ✅ ~18 | — | — | ✅ | — | | nylas/nylas-mail | — | — | ✅ ~193 | ✅ ~41 | — | — | | azat-co/react-quickly | — | — | ✅ ~155 | ✅ ~120 | — | — | @@ -155,6 +162,8 @@ This API was renamed to `useActionState` before it saw meaningful public adoptio **Minimum repos to cover all 5 viable codemods: 3** (zent + MetaMask + nylas-mail) +For modern React 18/19 coverage, `calcom/cal.com` is a strong fourth repo even though it is not required for minimum pattern coverage. + --- ## Test Execution Plan @@ -195,3 +204,4 @@ This API was renamed to `useActionState` before it saw meaningful public adoptio - Some repos are archived/unmaintained — that's fine for testing, the code patterns are what matter - `useFormState` testing is synthetic-only; recommend documenting this proactively for the React team - `useContext` is so widespread that any React 16.8+ repo works; no need for a dedicated test repo +- `calcom/cal.com` redirected to `calcom/cal.diy` on GitHub by 2026-04-17; keep both names in documentation for discoverability diff --git a/TESTING_REPORT.md b/TESTING_REPORT.md index d5808dc..87889bf 100644 --- a/TESTING_REPORT.md +++ b/TESTING_REPORT.md @@ -9,7 +9,7 @@ | **JSSG codemods** | `@react-new/*` (v0.1.0, published to Codemod Registry; regression fixes pending republish) | | **Legacy codemods** | `react/19/*` (jscodeshift, from Codemod Registry) | | **CLI** | `codemod@latest` with `--no-interactive` flag | -| **Test repos** | youzan/zent (React 17, TS), azat-co/react-quickly (React ~15, JS/JSX), atlassian/react-beautiful-dnd (React 16.13, JS+Flow) | +| **Test repos** | youzan/zent (React 17, TS), azat-co/react-quickly (React ~15, JS/JSX), atlassian/react-beautiful-dnd (React 16.13, JS+Flow), calcom/cal.diy (redirect from calcom/cal.com as of 2026-04-17, React 18/19 monorepo, tested at `v6.2.0` / `1c193cc`) | --- @@ -18,13 +18,13 @@ | Codemod | Verdict | JSSG Files | Legacy Files | Notes | |---------|---------|:----------:|:------------:|-------| | `replace-reactdom-render` | **Perfect parity** | 4 | 4 | Fixed: now handles `unmountComponentAtNode` + correct indentation | -| `replace-act-import` | **JSSG wins** | **6** | 1 | JSSG transforms 6× more files | -| `use-context-hook` | **Perfect parity** | 47 | 47 | Byte-identical diffs | +| `replace-act-import` | **JSSG wins** | **6** | 1 | JSSG transforms 6× more files on `react-beautiful-dnd`; `cal.com` `v6.2.0` is an additional 1-file parity spot-check | +| `use-context-hook` | **JSSG wins** | **30** | 29 | `youzan/zent` was byte-identical; `cal.com` adds 2 real call sites and avoids 1 unused-import false positive | | `replace-string-ref` | **JSSG wins** | **5** | 0 | Legacy skips `.jsx` files entirely | | `replace-use-form-state` | **Perfect parity** | 1 | 1 | Fixed: now moves import from `react-dom` to `react` | | `react-proptypes-to-prop-types` | No comparison | 2 | — | No legacy counterpart on registry | -**Bottom line**: 0 regressions, 2 areas where JSSG outperforms, 3 perfect parity, 1 unverifiable. +**Bottom line**: 0 regressions, 3 areas where JSSG outperforms, 2 perfect parity, 1 unverifiable. --- @@ -95,13 +95,29 @@ Both codemods produce the **same correct transformation** per file: But the legacy codemod only picks up **1 of 6 files** (`touch-sensor/click-blocking.spec.js`), while JSSG correctly finds and transforms all 6. The legacy codemod appears to have a file-matching or traversal limitation that causes it to miss files in nested test directories. +#### Additional spot-check: `calcom/cal.com` -> `calcom/cal.diy` (`v6.2.0`, commit `1c193cc`) + +As of April 17, 2026, GitHub redirects `calcom/cal.com` to `calcom/cal.diy`. On tag `v6.2.0`, both the local JSSG workflow and the legacy registry codemod transform the same single file: + +| Metric | JSSG | Legacy | +|--------|:----:|:------:| +| Files transformed | 1 | 1 | +| Diff comparison | **Byte-identical** | — | + +File transformed: `packages/ui/components/form/color-picker/colorpicker.test.tsx` + +```diff +-import { act } from "react-dom/test-utils"; ++import { act } from "react"; +``` + #### Recommendation No action needed — JSSG is strictly better here. --- -### 3. `use-context-hook` — **Perfect Parity** +### 3. `use-context-hook` — **Perfect Parity on zent; JSSG Outperforms on cal.com** **Repo**: youzan/zent (`packages/zent/src/`) @@ -124,9 +140,45 @@ Transformation pattern (applied consistently across all 47 files): +const value = use(SomeContext); ``` +#### Additional spot-check: `calcom/cal.com` -> `calcom/cal.diy` (`v6.2.0`, commit `1c193cc`) + +For this add-on validation, JSSG was run from the current local workflow in this repo and legacy was run from the published `react/19/use-context-hook` package. + +| Metric | JSSG | Legacy | +|--------|:----:|:------:| +| Files transformed | **30** | 29 | +| Insertions | **58** | 55 | +| Deletions | **58** | 55 | +| Common-file diff comparison | **28 files, byte-identical** | — | + +The overlapping 28 file diffs are byte-identical. The difference comes from three file-targeting decisions: + +- JSSG-only: `apps/web/modules/users/components/UserTable/BulkActions/MassAssignAttributes.tsx` +- JSSG-only: `packages/features/embed/lib/hooks/useEmbedDialogCtx.tsx` +- Legacy-only: `apps/web/modules/notifications/components/WebPushContext.tsx` + +The two JSSG-only files contain real `useContext(...)` call sites: + +```diff +- const context = useContext(AttributesContext); ++ const context = use(AttributesContext); +``` + +```diff +- const context = useContext(EmbedDialogContext); ++ const context = use(EmbedDialogContext); +``` + +The legacy-only file does **not** contain a `useContext(...)` call; it only had an unused `useContext` import, which legacy rewrote to `use` anyway: + +```diff +-import { createContext, useContext, useEffect, useMemo, useState } from "react"; ++import { createContext, use, useEffect, useMemo, useState } from "react"; +``` + #### Recommendation -No action needed — JSSG is at full parity. +No action needed — zent remains byte-identical parity, and cal.com shows a safe modern-repo improvement. --- @@ -240,6 +292,6 @@ All regressions found during initial testing have been fixed and retested. | Codemod | Status | |---------|--------| | `replace-act-import` | No action needed — JSSG outperforms legacy (6× coverage) | -| `use-context-hook` | No action needed — perfect byte-identical parity | +| `use-context-hook` | No action needed — zent is byte-identical and cal.com shows a safe extension (30 files vs 29, with 28 overlapping diffs identical) | | `replace-string-ref` | No action needed — JSSG outperforms legacy (handles `.jsx` files) | | `react-proptypes-to-prop-types` | No action needed — works correctly; no legacy to compare against | From 22b281a049117c4407d7efa34863c766603b86dd Mon Sep 17 00:00:00 2001 From: Mohab Sameh Date: Sat, 18 Apr 2026 00:40:43 +0200 Subject: [PATCH 29/29] fix copilot issues --- .../jssg/replace-reactdom-render/scripts/codemod.ts | 6 ++++-- .../tests/render-with-callback/expected.tsx | 8 ++++++++ .../tests/render-with-callback/input.tsx | 5 +++++ .../tests/render-with-callback/metrics.json | 11 +++++++++++ codemods/jssg/use-context-hook/scripts/codemod.ts | 6 +++++- .../tests/aliased-use-context-dollar/expected.tsx | 3 +++ .../tests/aliased-use-context-dollar/input.tsx | 3 +++ .../tests/aliased-use-context-dollar/metrics.json | 11 +++++++++++ 8 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 codemods/jssg/replace-reactdom-render/tests/render-with-callback/expected.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/render-with-callback/input.tsx create mode 100644 codemods/jssg/replace-reactdom-render/tests/render-with-callback/metrics.json create mode 100644 codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/expected.tsx create mode 100644 codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/input.tsx create mode 100644 codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/metrics.json diff --git a/codemods/jssg/replace-reactdom-render/scripts/codemod.ts b/codemods/jssg/replace-reactdom-render/scripts/codemod.ts index 1d5d4bb..558c2fc 100644 --- a/codemods/jssg/replace-reactdom-render/scripts/codemod.ts +++ b/codemods/jssg/replace-reactdom-render/scripts/codemod.ts @@ -97,7 +97,7 @@ const transform: Transform = async (root) => { const getIndent = (statement: SgNode): string => { const line = sourceLines[statement.range().start.line] ?? ""; const match = line.match(/^(\s*)/); - return match ? match[1] : ""; + return match?.[1] ?? ""; }; const applyRenderReplacement = (call: SgNode, pattern: string): void => { @@ -112,6 +112,7 @@ const transform: Transform = async (root) => { const element = argList[0]!; const container = argList[1]!; + const callback = argList[2]; const statement = call.ancestors().find((ancestor) => ancestor.kind() === "expression_statement"); if (!statement) return; @@ -130,7 +131,8 @@ const transform: Transform = async (root) => { renderCall = `${rootName}.render(${elementText})`; } - const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${renderCall};`; + const callbackCall = callback ? `\n${indent}(${callback.text()})();` : ""; + const replacement = `const ${rootName} = createRoot(${container.text()});\n${indent}${renderCall};${callbackCall}`; edits.push(statement.replace(replacement)); hasTransformations = true; transformMetric.increment({ diff --git a/codemods/jssg/replace-reactdom-render/tests/render-with-callback/expected.tsx b/codemods/jssg/replace-reactdom-render/tests/render-with-callback/expected.tsx new file mode 100644 index 0000000..78b9abf --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/render-with-callback/expected.tsx @@ -0,0 +1,8 @@ +import { createRoot } from "react-dom/client"; +import ReactDOM from "react-dom"; + +function mount(container) { + const root = createRoot(container); + root.render(); + (onReady)(); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/render-with-callback/input.tsx b/codemods/jssg/replace-reactdom-render/tests/render-with-callback/input.tsx new file mode 100644 index 0000000..aa80c80 --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/render-with-callback/input.tsx @@ -0,0 +1,5 @@ +import ReactDOM from "react-dom"; + +function mount(container) { + ReactDOM.render(, container, onReady); +} diff --git a/codemods/jssg/replace-reactdom-render/tests/render-with-callback/metrics.json b/codemods/jssg/replace-reactdom-render/tests/render-with-callback/metrics.json new file mode 100644 index 0000000..884e40c --- /dev/null +++ b/codemods/jssg/replace-reactdom-render/tests/render-with-callback/metrics.json @@ -0,0 +1,11 @@ +{ + "reactdom-render-replacements": [ + { + "cardinality": { + "file": "tests/render-with-callback/input.tsx", + "pattern": "ReactDOM.render" + }, + "count": 1 + } + ] +} \ No newline at end of file diff --git a/codemods/jssg/use-context-hook/scripts/codemod.ts b/codemods/jssg/use-context-hook/scripts/codemod.ts index 7e27b57..1bde92d 100644 --- a/codemods/jssg/use-context-hook/scripts/codemod.ts +++ b/codemods/jssg/use-context-hook/scripts/codemod.ts @@ -7,6 +7,10 @@ function metricFile(filename: string): string { return filename.startsWith(cwd) ? filename.slice(cwd.length) : filename; } +function escapeRegex(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + type NamedUseContextImport = { specifier: SgNode; localName: string }; function sourceText(node: SgNode): string | null { @@ -134,7 +138,7 @@ const transform: Transform = async (root) => { has: { field: "function", kind: "identifier", - regex: `^${localName}$`, + regex: `^${escapeRegex(localName)}$`, }, }, }); diff --git a/codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/expected.tsx b/codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/expected.tsx new file mode 100644 index 0000000..8142bc3 --- /dev/null +++ b/codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/expected.tsx @@ -0,0 +1,3 @@ +import { use as $ctx } from "react"; + +const value = $ctx(ThemeContext); diff --git a/codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/input.tsx b/codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/input.tsx new file mode 100644 index 0000000..4f5e735 --- /dev/null +++ b/codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/input.tsx @@ -0,0 +1,3 @@ +import { useContext as $ctx } from "react"; + +const value = $ctx(ThemeContext); diff --git a/codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/metrics.json b/codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/metrics.json new file mode 100644 index 0000000..79abe9f --- /dev/null +++ b/codemods/jssg/use-context-hook/tests/aliased-use-context-dollar/metrics.json @@ -0,0 +1,11 @@ +{ + "use-context-transformations": [ + { + "cardinality": { + "file": "tests/aliased-use-context-dollar/input.tsx", + "pattern": "useContext (aliased)" + }, + "count": 1 + } + ] +} \ No newline at end of file