diff --git a/.changeset/selfish-jars-film.md b/.changeset/selfish-jars-film.md index 0622f4cf..a8b08d4e 100644 --- a/.changeset/selfish-jars-film.md +++ b/.changeset/selfish-jars-film.md @@ -1,5 +1,5 @@ --- -"types-react-codemod": patch +"types-react-codemod": minor --- Add `deprecated-react-text` and `preset-19`. diff --git a/.changeset/warm-cobras-cough.md b/.changeset/warm-cobras-cough.md new file mode 100644 index 00000000..374dfb9f --- /dev/null +++ b/.changeset/warm-cobras-cough.md @@ -0,0 +1,7 @@ +--- +"types-react-codemod": minor +--- + +Add `deprecated-react-child` transform. + +Part of `preset-19`. diff --git a/README.md b/README.md index f6604128..68ab04a6 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,10 @@ $ npx types-react-codemod --help types-react-codemod Positionals: - codemod [string] [required] [choices: "context-any", "deprecated-react-text", - "deprecated-react-type", "deprecated-sfc-element", "deprecated-sfc", - "deprecated-stateless-component", "implicit-children", "preset-18", - "preset-19", "useCallback-implicit-any"] + codemod [string] [required] [choices: "context-any", "deprecated-react-child", + "deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element", + "deprecated-sfc", "deprecated-stateless-component", "implicit-children", + "preset-18", "preset-19", "useCallback-implicit-any"] paths [string] [required] Options: @@ -205,6 +205,28 @@ By default, the codemods that are definitely required to upgrade to `@types/reac The other codemods may or may not be required. You should select all and audit the changed files regardless. +### `deprecated-react-child` + +```diff + import * as React from "react"; + interface Props { +- label?: React.ReactChild; ++ label?: React.ReactElement | number | string; + } +``` + +#### `deprecated-react-text` false-negative pattern A + +Importing `ReactChild` via aliased named import will result in the transform being skipped. + +```tsx +import { ReactChild as MyReactChild } from "react"; +interface Props { + // not transformed + label?: MyReactChild; +} +``` + ### `deprecated-react-text` ```diff diff --git a/bin/__tests__/__snapshots__/types-react-codemod.js.snap b/bin/__tests__/__snapshots__/types-react-codemod.js.snap deleted file mode 100644 index 5f3ed211..00000000 --- a/bin/__tests__/__snapshots__/types-react-codemod.js.snap +++ /dev/null @@ -1,28 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`types-react-codemod provides help 1`] = ` -Object { - "stderr": "", - "stdout": "types-react-codemod - -Positionals: - codemod [string] [required] [choices: \\"context-any\\", \\"deprecated-react-text\\", - \\"deprecated-react-type\\", \\"deprecated-sfc-element\\", \\"deprecated-sfc\\", - \\"deprecated-stateless-component\\", \\"implicit-children\\", \\"preset-18\\", - \\"preset-19\\", \\"useCallback-implicit-any\\"] - paths [string] [required] - -Options: - --version Show version number [boolean] - --help Show help [boolean] - --dry [boolean] [default: false] - --ignore-pattern [string] [default: \\"**/node_modules/**\\"] - --verbose [boolean] [default: false] - -Examples: - types-react-codemod preset-18 ./ Ignores \`node_modules\` and \`build\` - --ignore-pattern folders - \\"**/{node_modules,build}/**\\" -", -} -`; diff --git a/bin/__tests__/types-react-codemod.js b/bin/__tests__/types-react-codemod.js index 3026cffc..ec0c82f4 100644 --- a/bin/__tests__/types-react-codemod.js +++ b/bin/__tests__/types-react-codemod.js @@ -14,7 +14,32 @@ describe("types-react-codemod", () => { } test("provides help", async () => { - // TODO: toMatchInlineSnapshot fails with "Couldn't locate all inline snapshots." - await expect(execTypesReactCodemod("--help")).resolves.toMatchSnapshot(); + await expect(execTypesReactCodemod("--help")).resolves + .toMatchInlineSnapshot(` + Object { + "stderr": "", + "stdout": "types-react-codemod + + Positionals: + codemod [string] [required] [choices: \\"context-any\\", \\"deprecated-react-child\\", + \\"deprecated-react-text\\", \\"deprecated-react-type\\", \\"deprecated-sfc-element\\", + \\"deprecated-sfc\\", \\"deprecated-stateless-component\\", \\"implicit-children\\", + \\"preset-18\\", \\"preset-19\\", \\"useCallback-implicit-any\\"] + paths [string] [required] + + Options: + --version Show version number [boolean] + --help Show help [boolean] + --dry [boolean] [default: false] + --ignore-pattern [string] [default: \\"**/node_modules/**\\"] + --verbose [boolean] [default: false] + + Examples: + types-react-codemod preset-18 ./ Ignores \`node_modules\` and \`build\` + --ignore-pattern folders + \\"**/{node_modules,build}/**\\" + ", + } + `); }); }); diff --git a/bin/types-react-codemod.cjs b/bin/types-react-codemod.cjs index 6dcd683b..d0c60b17 100755 --- a/bin/types-react-codemod.cjs +++ b/bin/types-react-codemod.cjs @@ -91,7 +91,10 @@ async function main() { message: "Pick transforms to apply", name: "presets", type: "checkbox", - choices: [{ checked: true, value: "deprecated-react-text" }], + choices: [ + { checked: true, value: "deprecated-react-child" }, + { checked: true, value: "deprecated-react-text" }, + ], }, ]); args.push(`--preset19Transforms="${presets.join(",")}"`); diff --git a/transforms/__tests__/deprecated-react-child.js b/transforms/__tests__/deprecated-react-child.js new file mode 100644 index 00000000..1dbe5a06 --- /dev/null +++ b/transforms/__tests__/deprecated-react-child.js @@ -0,0 +1,81 @@ +import { describe, expect, test } from "@jest/globals"; +import dedent from "dedent"; +import * as JscodeshiftTestUtils from "jscodeshift/dist/testUtils"; +import deprecatedReactChildTransform from "../deprecated-react-child"; + +function applyTransform(source, options = {}) { + return JscodeshiftTestUtils.applyTransform( + deprecatedReactChildTransform, + options, + { + path: "test.d.ts", + source: dedent(source), + } + ); +} + +describe("transform deprecated-react-child", () => { + test("not modified", () => { + expect( + applyTransform(` + import * as React from 'react'; + interface Props { + children?: ReactNode; + } + `) + ).toMatchInlineSnapshot(` + "import * as React from 'react'; + interface Props { + children?: ReactNode; + }" + `); + }); + + test("named import", () => { + expect( + applyTransform(` + import { ReactChild } from 'react'; + interface Props { + children?: ReactChild; + } + `) + ).toMatchInlineSnapshot(` + "import { ReactChild } from 'react'; + interface Props { + children?: React.ReactElement | number | string; + }" + `); + }); + + test("false-negative named renamed import", () => { + expect( + applyTransform(` + import { ReactChild as MyReactChild } from 'react'; + interface Props { + children?: MyReactChild; + } + `) + ).toMatchInlineSnapshot(` + "import { ReactChild as MyReactChild } from 'react'; + interface Props { + children?: MyReactChild; + }" + `); + }); + + test("namespace import", () => { + expect( + applyTransform(` + import * as React from 'react'; + interface Props { + children?: React.ReactChild; + } + `) + ).toMatchInlineSnapshot(` + "import * as React from 'react'; + interface Props { + children?: React.ReactElement | number | string; + }" + `); + }); +}); diff --git a/transforms/__tests__/preset-19.js b/transforms/__tests__/preset-19.js index d0a466c6..ab682131 100644 --- a/transforms/__tests__/preset-19.js +++ b/transforms/__tests__/preset-19.js @@ -4,6 +4,7 @@ import * as JscodeshiftTestUtils from "jscodeshift/dist/testUtils"; describe("preset-19", () => { let preset19Transform; + let deprecatedReactChildTransform; let deprecatedReactTextTransform; function applyTransform(source, options = {}) { @@ -29,6 +30,7 @@ describe("preset-19", () => { return transform; } + deprecatedReactChildTransform = mockTransform("../deprecated-react-child"); deprecatedReactTextTransform = mockTransform("../deprecated-react-text"); preset19Transform = require("../preset-19"); @@ -39,18 +41,19 @@ describe("preset-19", () => { preset19Transforms: "deprecated-react-text", }); + expect(deprecatedReactChildTransform).not.toHaveBeenCalled(); expect(deprecatedReactTextTransform).toHaveBeenCalled(); }); test("applies all", () => { applyTransform("", { - preset19Transforms: ["deprecated-react-text"].join(","), - }); - - applyTransform("", { - preset19Transforms: "deprecated-react-text", + preset19Transforms: [ + "deprecated-react-child", + "deprecated-react-text", + ].join(","), }); + expect(deprecatedReactChildTransform).toHaveBeenCalled(); expect(deprecatedReactTextTransform).toHaveBeenCalled(); }); }); diff --git a/transforms/deprecated-react-child.js b/transforms/deprecated-react-child.js new file mode 100644 index 00000000..054fe392 --- /dev/null +++ b/transforms/deprecated-react-child.js @@ -0,0 +1,47 @@ +const parseSync = require("./utils/parseSync"); + +/** + * @type {import('jscodeshift').Transform} + */ +const deprecatedReactChildTransform = (file, api) => { + const j = api.jscodeshift; + const ast = parseSync(file); + + const changedIdentifiers = ast + .find(j.TSTypeReference, (node) => { + const { typeName } = node; + /** + * @type {import('jscodeshift').Identifier | null} + */ + let identifier = null; + if (typeName.type === "Identifier") { + identifier = typeName; + } else if ( + typeName.type === "TSQualifiedName" && + typeName.right.type === "Identifier" + ) { + identifier = typeName.right; + } + + return identifier !== null && identifier.name === "ReactChild"; + }) + .replaceWith(() => { + // `React.ReactElement | number | string` + return j.tsUnionType([ + // React.ReactElement + j.tsTypeReference( + j.tsQualifiedName(j.identifier("React"), j.identifier("ReactElement")) + ), + j.tsNumberKeyword(), + j.tsStringKeyword(), + ]); + }); + + // Otherwise some files will be marked as "modified" because formatting changed + if (changedIdentifiers.length > 0) { + return ast.toSource(); + } + return file.source; +}; + +export default deprecatedReactChildTransform; diff --git a/transforms/preset-19.js b/transforms/preset-19.js index 97752446..79a83e41 100644 --- a/transforms/preset-19.js +++ b/transforms/preset-19.js @@ -1,3 +1,4 @@ +import deprecatedReactChildTransform from "./deprecated-react-child"; import deprecatedReactTextTransform from "./deprecated-react-text"; /** @@ -11,6 +12,9 @@ const transform = (file, api, options) => { * @type {import('jscodeshift').Transform[]} */ const transforms = []; + if (transformNames.has("deprecated-react-child")) { + transforms.push(deprecatedReactChildTransform); + } if (transformNames.has("deprecated-react-text")) { transforms.push(deprecatedReactTextTransform); }