diff --git a/.changeset/chilled-oranges-sit.md b/.changeset/chilled-oranges-sit.md index a1977526..0ee47b27 100644 --- a/.changeset/chilled-oranges-sit.md +++ b/.changeset/chilled-oranges-sit.md @@ -4,15 +4,15 @@ Ensure added imports of types use the `type` modifier -If we'd previously add an import to `ReactNode` (e.g. in `deprecated-react-fragment`), +If we'd previously add an import to `JSX` (e.g. in `scoped-jsx`), the codemod would import it as a value. This breaks TypeScript projects using `verbatimModuleSyntax` as well as projects enforcing `type` imports for types. Now we ensure new imports of types use the `type` modifier: ```diff --import { ReactNode } from 'react' -+import { type ReactNode } from 'react' +-import { JSX } from 'react' ++import { type JSX } from 'react' ``` This also changes how we transform the deprecated global JSX namespace. @@ -41,10 +41,4 @@ Note that rewriting of imports does not change the modifier. For example, the `deprecated-vfc-codemod` rewrites `VFC` identifiers to `FC`. If the import of `VFC` had no `type` modifier, the codemod will not add one. -This affects the following codemods: - -- `deprecated-react-fragment` -- `deprecated-react-node-array` -- `scoped-jsx` - `type` modifiers for import specifiers require [TypeScript 4.5 which has reached EOL](https://github.com/DefinitelyTyped/DefinitelyTyped#support-window in DefinitelyTyped) which is a strong signal that you should upgrade to at least TypeScript 4.6 by now. diff --git a/.changeset/famous-nails-destroy.md b/.changeset/famous-nails-destroy.md new file mode 100644 index 00000000..eb55016d --- /dev/null +++ b/.changeset/famous-nails-destroy.md @@ -0,0 +1,21 @@ +--- +"types-react-codemod": patch +--- + +Ensure replace and rename codemods have consistent behavior + +Fixes multiple incorrect transform patterns that were supported by some transforms but not others. +We no longer switch to `type` imports if the original type wasn't imported with that modifier. +Type parameters are now consistently preserved. +We don't add a reference to the `React` namespace anymore if we can just add a type import. + +This affects the following codemods: + +- `deprecated-legacy-ref` +- `deprecated-react-child` +- `deprecated-react-text` +- `deprecated-react-type` +- `deprecated-sfc-element` +- `deprecated-sfc` +- `deprecated-stateless-component` +- `deprecated-void-function-component` diff --git a/.changeset/short-zoos-type.md b/.changeset/short-zoos-type.md index 00850b3a..6ada46bb 100644 --- a/.changeset/short-zoos-type.md +++ b/.changeset/short-zoos-type.md @@ -9,7 +9,5 @@ Now we properly detect that e.g. `JSX` is used in `someFunctionWithTypeParameter Affected codemods: - `deprecated-react-child` -- `deprecated-react-fragment` -- `deprecated-react-node-array` - `deprecated-react-text` - `scoped-jsx` diff --git a/transforms/__tests__/deprecated-react-child.js b/transforms/__tests__/deprecated-react-child.js index 0e72b70e..355e59a7 100644 --- a/transforms/__tests__/deprecated-react-child.js +++ b/transforms/__tests__/deprecated-react-child.js @@ -1,4 +1,4 @@ -const { describe, expect, test } = require("@jest/globals"); +const { expect, test } = require("@jest/globals"); const dedent = require("dedent"); const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils"); const deprecatedReactChildTransform = require("../deprecated-react-child"); @@ -14,80 +14,110 @@ function applyTransform(source, options = {}) { ); } -describe("transform deprecated-react-child", () => { - test("not modified", () => { - expect( - applyTransform(` +test("not modified", () => { + expect( + applyTransform(` import * as React from 'react'; interface Props { children?: ReactNode; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; interface Props { children?: ReactNode; }" `); - }); +}); - test("named import", () => { - expect( - applyTransform(` +test("named import", () => { + expect( + applyTransform(` import { ReactChild } from 'react'; interface Props { children?: ReactChild; } `), - ).toMatchInlineSnapshot(` - "import { ReactChild } from 'react'; + ).toMatchInlineSnapshot(` + "import { ReactElement } from 'react'; + interface Props { + children?: ReactElement | number | string; + }" + `); +}); + +test("named type import", () => { + expect( + applyTransform(` + import { type ReactChild } from 'react'; + interface Props { + children?: ReactChild; + } + `), + ).toMatchInlineSnapshot(` + "import { type ReactElement } from 'react'; + interface Props { + children?: ReactElement | number | string; + }" + `); +}); + +test("named type import with existing target import", () => { + expect( + applyTransform(` + import { ReactChild, ReactElement } from 'react'; + interface Props { + children?: ReactChild; + } + `), + ).toMatchInlineSnapshot(` + "import { ReactElement } from 'react'; interface Props { - children?: React.ReactElement | number | string; + children?: ReactElement | number | string; }" `); - }); +}); - test("false-negative named renamed import", () => { - expect( - applyTransform(` +test("false-negative named renamed import", () => { + expect( + applyTransform(` import { ReactChild as MyReactChild } from 'react'; interface Props { children?: MyReactChild; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { ReactChild as MyReactChild } from 'react'; interface Props { children?: MyReactChild; }" `); - }); +}); - test("namespace import", () => { - expect( - applyTransform(` +test("namespace import", () => { + expect( + applyTransform(` import * as React from 'react'; interface Props { children?: React.ReactChild; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; interface Props { children?: React.ReactElement | number | string; }" `); - }); +}); - test("as type parameter", () => { - expect( - applyTransform(` +test("as type parameter", () => { + expect( + applyTransform(` import * as React from 'react'; createAction() `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; createAction()" `); - }); }); diff --git a/transforms/__tests__/deprecated-react-fragment.js b/transforms/__tests__/deprecated-react-fragment.js index ff474eee..31451204 100644 --- a/transforms/__tests__/deprecated-react-fragment.js +++ b/transforms/__tests__/deprecated-react-fragment.js @@ -1,4 +1,4 @@ -const { describe, expect, test } = require("@jest/globals"); +const { expect, test } = require("@jest/globals"); const dedent = require("dedent"); const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils"); const deprecatedReactFragmentTransform = require("../deprecated-react-fragment"); @@ -14,128 +14,126 @@ function applyTransform(source, options = {}) { ); } -describe("transform deprecated-react-node-array", () => { - test("not modified", () => { - expect( - applyTransform(` +test("not modified", () => { + expect( + applyTransform(` import * as React from 'react'; interface Props { children?: ReactNode; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; interface Props { children?: ReactNode; }" `); - }); +}); - test("named import", () => { - expect( - applyTransform(` +test("named import", () => { + expect( + applyTransform(` import { ReactFragment } from 'react'; interface Props { children?: ReactFragment; } `), - ).toMatchInlineSnapshot(` - "import { type ReactNode } from 'react'; + ).toMatchInlineSnapshot(` + "import { ReactNode } from 'react'; interface Props { children?: Iterable; }" `); - }); +}); - test("named type import", () => { - expect( - applyTransform(` - import type { ReactFragment } from 'react'; +test("named type import", () => { + expect( + applyTransform(` + import { type ReactFragment } from 'react'; interface Props { children?: ReactFragment; } `), - ).toMatchInlineSnapshot(` - "import type { ReactNode } from 'react'; + ).toMatchInlineSnapshot(` + "import { type ReactNode } from 'react'; interface Props { children?: Iterable; }" `); - }); +}); - test("named import with existing ReactNode import", () => { - expect( - applyTransform(` +test("named import with existing target import", () => { + expect( + applyTransform(` import { ReactFragment, ReactNode } from 'react'; interface Props { children?: ReactFragment; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { ReactNode } from 'react'; interface Props { children?: Iterable; }" `); - }); +}); - test("named import with existing ReactNode type import", () => { - expect( - applyTransform(` +test("named import with existing ReactNode type import", () => { + expect( + applyTransform(` import { ReactFragment, type ReactNode } from 'react'; interface Props { children?: ReactFragment; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { type ReactNode } from 'react'; interface Props { children?: Iterable; }" `); - }); +}); - test("false-negative named renamed import", () => { - expect( - applyTransform(` +test("false-negative named renamed import", () => { + expect( + applyTransform(` import { ReactFragment as MyReactFragment } from 'react'; interface Props { children?: MyReactFragment; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { ReactFragment as MyReactFragment } from 'react'; interface Props { children?: MyReactFragment; }" `); - }); +}); - test("namespace import", () => { - expect( - applyTransform(` +test("namespace import", () => { + expect( + applyTransform(` import * as React from 'react'; interface Props { children?: React.ReactFragment; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; interface Props { children?: Iterable; }" `); - }); +}); - test("as type parameter", () => { - expect( - applyTransform(` +test("as type parameter", () => { + expect( + applyTransform(` import * as React from 'react'; createComponent(); `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; createComponent>();" `); - }); }); diff --git a/transforms/__tests__/deprecated-react-node-array.js b/transforms/__tests__/deprecated-react-node-array.js index b9e32047..725fcef8 100644 --- a/transforms/__tests__/deprecated-react-node-array.js +++ b/transforms/__tests__/deprecated-react-node-array.js @@ -1,4 +1,4 @@ -const { describe, expect, test } = require("@jest/globals"); +const { expect, test } = require("@jest/globals"); const dedent = require("dedent"); const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils"); const deprecatedReactNodeArrayTransform = require("../deprecated-react-node-array"); @@ -14,128 +14,126 @@ function applyTransform(source, options = {}) { ); } -describe("transform deprecated-react-node-array", () => { - test("not modified", () => { - expect( - applyTransform(` +test("not modified", () => { + expect( + applyTransform(` import * as React from 'react'; interface Props { children?: ReactNode; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; interface Props { children?: ReactNode; }" `); - }); +}); - test("named import", () => { - expect( - applyTransform(` +test("named import", () => { + expect( + applyTransform(` import { ReactNodeArray } from 'react'; interface Props { children?: ReactNodeArray; } `), - ).toMatchInlineSnapshot(` - "import { type ReactNode } from 'react'; + ).toMatchInlineSnapshot(` + "import { ReactNode } from 'react'; interface Props { children?: ReadonlyArray; }" `); - }); +}); - test("named type import", () => { - expect( - applyTransform(` - import type { ReactNodeArray } from 'react'; +test("named type import", () => { + expect( + applyTransform(` + import { type ReactNodeArray } from 'react'; interface Props { children?: ReactNodeArray; } `), - ).toMatchInlineSnapshot(` - "import type { ReactNode } from 'react'; + ).toMatchInlineSnapshot(` + "import { type ReactNode } from 'react'; interface Props { children?: ReadonlyArray; }" `); - }); +}); - test("named import with existing ReactNode import", () => { - expect( - applyTransform(` +test("named import with existing target import", () => { + expect( + applyTransform(` import { ReactNodeArray, ReactNode } from 'react'; interface Props { children?: ReactNodeArray; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { ReactNode } from 'react'; interface Props { children?: ReadonlyArray; }" `); - }); +}); - test("named import with existing ReactNode type import", () => { - expect( - applyTransform(` +test("named import with existing target type import", () => { + expect( + applyTransform(` import { ReactNodeArray, type ReactNode } from 'react'; interface Props { children?: ReactNodeArray; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { type ReactNode } from 'react'; interface Props { children?: ReadonlyArray; }" `); - }); +}); - test("false-negative named renamed import", () => { - expect( - applyTransform(` +test("false-negative named renamed import", () => { + expect( + applyTransform(` import { ReactNodeArray as MyReactNodeArray } from 'react'; interface Props { children?: MyReactNodeArray; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { ReactNodeArray as MyReactNodeArray } from 'react'; interface Props { children?: MyReactNodeArray; }" `); - }); +}); - test("namespace import", () => { - expect( - applyTransform(` +test("namespace import", () => { + expect( + applyTransform(` import * as React from 'react'; interface Props { children?: React.ReactNodeArray; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; interface Props { children?: ReadonlyArray; }" `); - }); +}); - test("in type parameters", () => { - expect( - applyTransform(` +test("in type parameters", () => { + expect( + applyTransform(` import * as React from 'react'; createComponent(); `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; createComponent>();" `); - }); }); diff --git a/transforms/__tests__/deprecated-react-text.js b/transforms/__tests__/deprecated-react-text.js index 3b2d29f6..13ae94e1 100644 --- a/transforms/__tests__/deprecated-react-text.js +++ b/transforms/__tests__/deprecated-react-text.js @@ -1,4 +1,4 @@ -const { describe, expect, test } = require("@jest/globals"); +const { expect, test } = require("@jest/globals"); const dedent = require("dedent"); const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils"); const deprecatedReactTextTransform = require("../deprecated-react-text"); @@ -14,80 +14,94 @@ function applyTransform(source, options = {}) { ); } -describe("transform deprecated-react-text", () => { - test("not modified", () => { - expect( - applyTransform(` +test("not modified", () => { + expect( + applyTransform(` import * as React from 'react'; interface Props { children?: ReactNode; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; interface Props { children?: ReactNode; }" `); - }); +}); - test("named import", () => { - expect( - applyTransform(` +test("named import", () => { + expect( + applyTransform(` import { ReactText } from 'react'; interface Props { children?: ReactText; } `), - ).toMatchInlineSnapshot(` - "import { ReactText } from 'react'; + ).toMatchInlineSnapshot(` + "import 'react'; + interface Props { + children?: number | string; + }" + `); +}); + +test("named type import", () => { + expect( + applyTransform(` + import { type ReactText } from 'react'; + interface Props { + children?: ReactText; + } + `), + ).toMatchInlineSnapshot(` + "import 'react'; interface Props { children?: number | string; }" `); - }); +}); - test("false-negative named renamed import", () => { - expect( - applyTransform(` +test("false-negative named renamed import", () => { + expect( + applyTransform(` import { ReactText as MyReactText } from 'react'; interface Props { children?: MyReactText; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { ReactText as MyReactText } from 'react'; interface Props { children?: MyReactText; }" `); - }); +}); - test("namespace import", () => { - expect( - applyTransform(` +test("namespace import", () => { + expect( + applyTransform(` import * as React from 'react'; interface Props { children?: React.ReactText; } `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; interface Props { children?: number | string; }" `); - }); +}); - test("in type parameters", () => { - expect( - applyTransform(` +test("in type parameters", () => { + expect( + applyTransform(` import * as React from 'react'; createComponent(); `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; createComponent();" `); - }); }); diff --git a/transforms/__tests__/deprecated-react-type.js b/transforms/__tests__/deprecated-react-type.js index 9053eb01..592345c8 100644 --- a/transforms/__tests__/deprecated-react-type.js +++ b/transforms/__tests__/deprecated-react-type.js @@ -1,4 +1,4 @@ -const { describe, expect, test } = require("@jest/globals"); +const { expect, test } = require("@jest/globals"); const dedent = require("dedent"); const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils"); const deprecatedReactTypeTransform = require("../deprecated-react-type"); @@ -14,64 +14,110 @@ function applyTransform(source, options = {}) { ); } -describe("transform deprecated-react-type", () => { - test("not modified", () => { - expect( - applyTransform(` +test("not modified", () => { + expect( + applyTransform(` import { ElementType } from 'react'; - ElementType; + declare const a: ElementType; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { ElementType } from 'react'; - ElementType;" + declare const a: ElementType;" `); - }); +}); - test("named import", () => { - expect( - applyTransform(` - import { ReactType } from 'react'; - ReactType; +test("named import", () => { + expect( + applyTransform(` + import { ReactType } from 'react'; + declare const a: ReactType; + declare const b: ReactType; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { ElementType } from 'react'; - ElementType;" + declare const a: ElementType; + declare const b: ElementType;" + `); +}); + +test("named type import", () => { + expect( + applyTransform(` + import { type ReactType } from 'react'; + declare const a: ReactType; + declare const b: ReactType; + `), + ).toMatchInlineSnapshot(` + "import { type ElementType } from 'react'; + declare const a: ElementType; + declare const b: ElementType;" `); - }); +}); - test("named renamed import", () => { - expect( - applyTransform(` - import { ReactType as MyReactType } from 'react'; - MyReactType; +test("named type import with existing target import", () => { + expect( + applyTransform(` + import { type ReactType, ElementType } from 'react'; + declare const a: ReactType; + declare const b: ReactType; `), - ).toMatchInlineSnapshot(` - "import { ElementType as MyReactType } from 'react'; - MyReactType;" + ).toMatchInlineSnapshot(` + "import { ElementType } from 'react'; + declare const a: ElementType; + declare const b: ElementType;" `); - }); +}); - test("namespace import", () => { - expect( - applyTransform(` +test("false-negative named renamed import", () => { + expect( + applyTransform(` + import { ReactType as MyReactType } from 'react'; + declare const a: MyReactType; + declare const b: MyReactType; + `), + ).toMatchInlineSnapshot(` + "import { ReactType as MyReactType } from 'react'; + declare const a: MyReactType; + declare const b: MyReactType;" + `); +}); + +test("namespace import", () => { + expect( + applyTransform(` import * as React from 'react'; - React.ReactType; + declare const a: React.ReactType; + declare const b: React.ReactType; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; - React.ElementType;" + declare const a: React.ElementType; + declare const b: React.ElementType;" `); - }); +}); - test("false-positive rename on different namespace", () => { - expect( - applyTransform(` +test("false-positive rename on different namespace", () => { + expect( + applyTransform(` import * as Preact from 'preact'; - Preact.ReactType; + declare const a: Preact.ReactType; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as Preact from 'preact'; - Preact.ElementType;" + declare const a: Preact.ElementType;" + `); +}); + +test("as type parameter", () => { + expect( + applyTransform(` + import * as React from 'react'; + createComponent(); + createComponent>(); + `), + ).toMatchInlineSnapshot(` + "import * as React from 'react'; + createComponent(); + createComponent>();" `); - }); }); diff --git a/transforms/__tests__/deprecated-sfc-element.js b/transforms/__tests__/deprecated-sfc-element.js index c00a58f0..e60360ac 100644 --- a/transforms/__tests__/deprecated-sfc-element.js +++ b/transforms/__tests__/deprecated-sfc-element.js @@ -1,4 +1,4 @@ -const { describe, expect, test } = require("@jest/globals"); +const { expect, test } = require("@jest/globals"); const dedent = require("dedent"); const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils"); const deprecatedSFCElementTransform = require("../deprecated-sfc-element"); @@ -14,76 +14,94 @@ function applyTransform(source, options = {}) { ); } -describe("transform deprecated-sfc-element", () => { - test("not modified", () => { - expect( - applyTransform(` +test("not modified", () => { + expect( + applyTransform(` import { FunctionComponentElement } from 'react'; - FunctionComponentElement; + declare const a: FunctionComponentElement; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { FunctionComponentElement } from 'react'; - FunctionComponentElement;" + declare const a: FunctionComponentElement;" `); - }); +}); - test("named import", () => { - expect( - applyTransform(` +test("named import", () => { + expect( + applyTransform(` import { SFCElement } from 'react'; - SFCElement; + declare const a: SFCElement; + declare const b: SFCElement; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { FunctionComponentElement } from 'react'; - FunctionComponentElement;" + declare const a: FunctionComponentElement; + declare const b: FunctionComponentElement;" `); - }); +}); - test("named typew import", () => { - expect( - applyTransform(` +test("named type import", () => { + expect( + applyTransform(` import { type SFCElement } from 'react'; - SFCElement; + declare const a: SFCElement; + declare const b: SFCElement; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { type FunctionComponentElement } from 'react'; - FunctionComponentElement;" + declare const a: FunctionComponentElement; + declare const b: FunctionComponentElement;" `); - }); +}); - test("named renamed import", () => { - expect( - applyTransform(` +test("false-negative named renamed import", () => { + expect( + applyTransform(` import { SFCElement as MySFCElement } from 'react'; MySFCElement; `), - ).toMatchInlineSnapshot(` - "import { FunctionComponentElement as MySFCElement } from 'react'; + ).toMatchInlineSnapshot(` + "import { SFCElement as MySFCElement } from 'react'; MySFCElement;" `); - }); +}); - test("namespace import", () => { - expect( - applyTransform(` +test("namespace import", () => { + expect( + applyTransform(` import * as React from 'react'; - React.SFCElement; + declare const a: React.SFCElement; + declare const b: React.SFCElement; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; - React.FunctionComponentElement;" + declare const a: React.FunctionComponentElement; + declare const b: React.FunctionComponentElement;" `); - }); +}); - test("false-positive rename on different namespace", () => { - expect( - applyTransform(` +test("false-positive rename on different namespace", () => { + expect( + applyTransform(` import * as Preact from 'preact'; - Preact.SFCElement; + declare const b: Preact.SFCElement; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as Preact from 'preact'; - Preact.FunctionComponentElement;" + declare const b: Preact.FunctionComponentElement;" + `); +}); + +test("as type parameter", () => { + expect( + applyTransform(` + import * as React from 'react'; + createComponent(); + createComponent>(); + `), + ).toMatchInlineSnapshot(` + "import * as React from 'react'; + createComponent(); + createComponent>();" `); - }); }); diff --git a/transforms/__tests__/deprecated-sfc.js b/transforms/__tests__/deprecated-sfc.js index 3462df4c..29e00aba 100644 --- a/transforms/__tests__/deprecated-sfc.js +++ b/transforms/__tests__/deprecated-sfc.js @@ -1,4 +1,4 @@ -const { describe, expect, test } = require("@jest/globals"); +const { expect, test } = require("@jest/globals"); const dedent = require("dedent"); const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils"); const deprecatedSFCTransform = require("../deprecated-sfc"); @@ -10,76 +10,112 @@ function applyTransform(source, options = {}) { }); } -describe("transform deprecated-sfc", () => { - test("not modified", () => { - expect( - applyTransform(` +test("not modified", () => { + expect( + applyTransform(` import { FC } from 'react'; - FC; + declare const a: FC; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { FC } from 'react'; - FC;" + declare const a: FC;" `); - }); +}); - test("named import", () => { - expect( - applyTransform(` +test("named import", () => { + expect( + applyTransform(` import { SFC } from 'react'; - SFC; + declare const a: SFC; + declare const b: SFC; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { FC } from 'react'; - FC;" + declare const a: FC; + declare const b: FC;" `); - }); +}); - test("named type import", () => { - expect( - applyTransform(` +test("named type import", () => { + expect( + applyTransform(` import { type SFC } from 'react'; - SFC; + declare const a: SFC; + declare const b: SFC; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { type FC } from 'react'; - FC;" + declare const a: FC; + declare const b: FC;" `); - }); +}); + +test("named import with existing target import", () => { + expect( + applyTransform(` + import { SFC } from 'react'; + declare const a: SFC; + declare const b: SFC; + `), + ).toMatchInlineSnapshot(` + "import { FC } from 'react'; + declare const a: FC; + declare const b: FC;" + `); +}); - test("named renamed import", () => { - expect( - applyTransform(` +test("false-negative named renamed import", () => { + expect( + applyTransform(` import { SFC as MySFC } from 'react'; - MySFCElement; + declare const a: MySFC; + declare const b: MySFC; `), - ).toMatchInlineSnapshot(` - "import { FC as MySFC } from 'react'; - MySFCElement;" + ).toMatchInlineSnapshot(` + "import { SFC as MySFC } from 'react'; + declare const a: MySFC; + declare const b: MySFC;" `); - }); +}); - test("namespace import", () => { - expect( - applyTransform(` +test("namespace import", () => { + expect( + applyTransform(` import * as React from 'react'; - React.SFC; + declare const a: React.SFC; + declare const b: React.SFC; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; - React.FC;" + declare const a: React.FC; + declare const b: React.FC;" `); - }); +}); - test("false-positive rename on different namespace", () => { - expect( - applyTransform(` +test("false-positive rename on different namespace", () => { + expect( + applyTransform(` import * as Preact from 'preact'; - Preact.SFC; + declare const a: Preact.SFC; + declare const b: Preact.SFC; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as Preact from 'preact'; - Preact.FC;" + declare const a: Preact.FC; + declare const b: Preact.FC;" + `); +}); + +test("as type parameter", () => { + expect( + applyTransform(` + import * as React from 'react'; + createComponent(); + createComponent>(); + `), + ).toMatchInlineSnapshot(` + "import * as React from 'react'; + createComponent(); + createComponent>();" `); - }); }); diff --git a/transforms/__tests__/deprecated-stateless-component.js b/transforms/__tests__/deprecated-stateless-component.js index 5262b50c..cd1582c1 100644 --- a/transforms/__tests__/deprecated-stateless-component.js +++ b/transforms/__tests__/deprecated-stateless-component.js @@ -1,4 +1,4 @@ -const { describe, expect, test } = require("@jest/globals"); +const { expect, test } = require("@jest/globals"); const dedent = require("dedent"); const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils"); const deprecatedStatelessComponent = require("../deprecated-stateless-component"); @@ -14,76 +14,108 @@ function applyTransform(source, options = {}) { ); } -describe("transform deprecated-stateless-component", () => { - test("not modified", () => { - expect( - applyTransform(` +test("not modified", () => { + expect( + applyTransform(` import { FunctionComponent } from 'react'; - FunctionComponent; + declare const a: FunctionComponent; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { FunctionComponent } from 'react'; - FunctionComponent;" + declare const a: FunctionComponent;" `); - }); +}); - test("named import", () => { - expect( - applyTransform(` +test("named import", () => { + expect( + applyTransform(` import { StatelessComponent } from 'react'; - StatelessComponent; + declare const a: StatelessComponent; + declare const b: StatelessComponent; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { FunctionComponent } from 'react'; - FunctionComponent;" + declare const a: FunctionComponent; + declare const b: FunctionComponent;" `); - }); +}); - test("named type import", () => { - expect( - applyTransform(` +test("named type import", () => { + expect( + applyTransform(` import { type StatelessComponent } from 'react'; - StatelessComponent; + declare const a: StatelessComponent; + declare const b: StatelessComponent; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { type FunctionComponent } from 'react'; - FunctionComponent;" + declare const a: FunctionComponent; + declare const b: FunctionComponent;" + `); +}); + +test("named import with existing target import", () => { + expect( + applyTransform(` + import { StatelessComponent, FunctionComponent } from 'react'; + declare const a: StatelessComponent; + declare const b: StatelessComponent; + `), + ).toMatchInlineSnapshot(` + "import { FunctionComponent } from 'react'; + declare const a: FunctionComponent; + declare const b: FunctionComponent;" `); - }); +}); - test("named renamed import", () => { - expect( - applyTransform(` +test("named renamed import", () => { + expect( + applyTransform(` import { StatelessComponent as MyStatelessComponent } from 'react'; - MyStatelessComponent; + declare const a: MyStatelessComponent; + declare const b: MyStatelessComponent; `), - ).toMatchInlineSnapshot(` - "import { FunctionComponent as MyStatelessComponent } from 'react'; - MyStatelessComponent;" + ).toMatchInlineSnapshot(` + "import { StatelessComponent as MyStatelessComponent } from 'react'; + declare const a: MyStatelessComponent; + declare const b: MyStatelessComponent;" `); - }); +}); - test("namespace import", () => { - expect( - applyTransform(` +test("namespace import", () => { + expect( + applyTransform(` import * as React from 'react'; - React.StatelessComponent; + declare const a: React.StatelessComponent; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; - React.FunctionComponent;" + declare const a: React.FunctionComponent;" `); - }); +}); - test("false-positive rename on different namespace", () => { - expect( - applyTransform(` +test("false-positive rename on different namespace", () => { + expect( + applyTransform(` import * as Preact from 'preact'; - Preact.StatelessComponent; + declare const a: Preact.StatelessComponent; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as Preact from 'preact'; - Preact.FunctionComponent;" + declare const a: Preact.FunctionComponent;" + `); +}); + +test("as type parameter", () => { + expect( + applyTransform(` + import * as React from 'react'; + createComponent(); + createComponent>(); + `), + ).toMatchInlineSnapshot(` + "import * as React from 'react'; + createComponent(); + createComponent>();" `); - }); }); diff --git a/transforms/__tests__/deprecated-void-function-component.js b/transforms/__tests__/deprecated-void-function-component.js index 380fa282..ad6c3877 100644 --- a/transforms/__tests__/deprecated-void-function-component.js +++ b/transforms/__tests__/deprecated-void-function-component.js @@ -1,4 +1,4 @@ -const { describe, expect, test } = require("@jest/globals"); +const { expect, test } = require("@jest/globals"); const dedent = require("dedent"); const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils"); const deprecatedVoidFunctionComponentTransform = require("../deprecated-void-function-component"); @@ -14,86 +14,136 @@ function applyTransform(source, options = {}) { ); } -describe("transform deprecated-void-function-component", () => { - test("not modified", () => { - expect( - applyTransform(` +test("not modified", () => { + expect( + applyTransform(` import { FC } from 'react'; - FC; + declare const a: FC; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import { FC } from 'react'; - FC;" + declare const a: FC;" `); - }); +}); - test("named import", () => { - expect( - applyTransform(` +test("named import", () => { + expect( + applyTransform(` import { VFC, VoidFunctionComponent } from 'react'; - VFC; - VoidFunctionComponent; + declare const a: VFC; + declare const b: VFC; + declare const c: VoidFunctionComponent; + declare const d: VoidFunctionComponent; `), - ).toMatchInlineSnapshot(` - "import { FC, FunctionComponent } from 'react'; - FC; - FunctionComponent;" + ).toMatchInlineSnapshot(` + "import { FC, VoidFunctionComponent } from 'react'; + declare const a: FC; + declare const b: FC; + declare const c: VoidFunctionComponent; + declare const d: VoidFunctionComponent;" `); - }); +}); - test("named type import", () => { - expect( - applyTransform(` +test("named type import", () => { + expect( + applyTransform(` import { type VFC, type VoidFunctionComponent } from 'react'; - VFC; - VoidFunctionComponent; + declare const a: VFC; + declare const b: VFC; + declare const c: VoidFunctionComponent; + declare const d: VoidFunctionComponent; `), - ).toMatchInlineSnapshot(` - "import { type FC, type FunctionComponent } from 'react'; - FC; - FunctionComponent;" + ).toMatchInlineSnapshot(` + "import { type FC, type VoidFunctionComponent } from 'react'; + declare const a: FC; + declare const b: FC; + declare const c: VoidFunctionComponent; + declare const d: VoidFunctionComponent;" `); - }); +}); + +test("named import with existing target import", () => { + expect( + applyTransform(` + import { VFC, VoidFunctionComponent, FC, FunctionComponent } from 'react'; + declare const a: VFC; + declare const b: VFC; + declare const c: VoidFunctionComponent; + declare const d: VoidFunctionComponent; + `), + ).toMatchInlineSnapshot(` + "import { VoidFunctionComponent, FC, FunctionComponent } from 'react'; + declare const a: FC; + declare const b: FC; + declare const c: VoidFunctionComponent; + declare const d: VoidFunctionComponent;" + `); +}); - test("named renamed import", () => { - expect( - applyTransform(` +test("false-negative named renamed import", () => { + expect( + applyTransform(` import { VFC as MyVFC, VoidFunctionComponent as MyVoidFunctionComponent } from 'react'; - MyVFC; - MyVoidFunctionComponent; + declare const a: MyVFC; + declare const b: MyVFC; + declare const c: MyVoidFunctionComponent; + declare const d: MyVoidFunctionComponent; `), - ).toMatchInlineSnapshot(` - "import { FC as MyVFC, FunctionComponent as MyVoidFunctionComponent } from 'react'; - MyVFC; - MyVoidFunctionComponent;" + ).toMatchInlineSnapshot(` + "import { VFC as MyVFC, VoidFunctionComponent as MyVoidFunctionComponent } from 'react'; + declare const a: MyVFC; + declare const b: MyVFC; + declare const c: MyVoidFunctionComponent; + declare const d: MyVoidFunctionComponent;" `); - }); +}); - test("namespace import", () => { - expect( - applyTransform(` +test("namespace import", () => { + expect( + applyTransform(` import * as React from 'react'; - React.VFC; - React.VoidFunctionComponent; + declare const a: React.VFC; + declare const b: React.VFC; + declare const c: React.VoidFunctionComponent; + declare const d: React.VoidFunctionComponent; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as React from 'react'; - React.FC; - React.FunctionComponent;" + declare const a: React.FC; + declare const b: React.FC; + declare const c: React.VoidFunctionComponent; + declare const d: React.VoidFunctionComponent;" `); - }); +}); - test("false-positive rename on different namespace", () => { - expect( - applyTransform(` +test("false-positive rename on different namespace", () => { + expect( + applyTransform(` import * as Preact from 'preact'; - Preact.VFC; - Preact.VoidFunctionComponent; + declare const a: Preact.VFC; + declare const b: Preact.VoidFunctionComponent; `), - ).toMatchInlineSnapshot(` + ).toMatchInlineSnapshot(` "import * as Preact from 'preact'; - Preact.FC; - Preact.FunctionComponent;" + declare const a: Preact.FC; + declare const b: Preact.VoidFunctionComponent;" + `); +}); + +test("as type parameter", () => { + expect( + applyTransform(` + import * as React from 'react'; + createComponent(); + createComponent>(); + createComponent(); + createComponent>(); + `), + ).toMatchInlineSnapshot(` + "import * as React from 'react'; + createComponent(); + createComponent>(); + createComponent(); + createComponent>();" `); - }); }); diff --git a/transforms/deprecated-legacy-ref.js b/transforms/deprecated-legacy-ref.js index 2588c35d..5b1987b6 100644 --- a/transforms/deprecated-legacy-ref.js +++ b/transforms/deprecated-legacy-ref.js @@ -1,7 +1,5 @@ const parseSync = require("./utils/parseSync"); -const { - findTSTypeReferenceCollections, -} = require("./utils/jscodeshift-bugfixes"); +const { renameType } = require("./utils/replaceType"); /** * @type {import('jscodeshift').Transform} @@ -10,90 +8,7 @@ const deprecatedLegacyRefTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - let hasChanges = false; - - const targetIdentifierImports = ast.find(j.ImportSpecifier, (node) => { - const { imported, local } = node; - return ( - imported.type === "Identifier" && - imported.name === "Ref" && - // We don't support renames generally, so we don't handle them here - (local == null || local.name === "Ref") - ); - }); - const sourceIdentifierImports = ast.find(j.ImportSpecifier, (node) => { - const { imported, local } = node; - return ( - imported.type === "Identifier" && - imported.name === "LegacyRef" && - // We don't support renames generally, so we don't handle them here - (local == null || local.name === "LegacyRef") - ); - }); - if (targetIdentifierImports.length > 0) { - hasChanges = true; - sourceIdentifierImports.remove(); - } else if (sourceIdentifierImports.length > 0) { - hasChanges = true; - sourceIdentifierImports.replaceWith((path) => { - const importSpecifier = j.importSpecifier(j.identifier("Ref")); - if ("importKind" in path.node) { - // @ts-expect-error -- Missing types in jscodeshift. Babel uses `importKind`: https://astexplorer.net/#/gist/a76bd35f28483a467fef29d3c63aac9b/0e7ba6688fc09bd11b92197349b2384bb4c94574 - importSpecifier.importKind = path.node.importKind; - } - - return importSpecifier; - }); - } - - const sourceIdentifierTypeReferences = findTSTypeReferenceCollections( - j, - ast, - (node) => { - const { typeName } = node; - - return typeName.type === "Identifier" && typeName.name === "LegacyRef"; - }, - ); - for (const typeReferences of sourceIdentifierTypeReferences) { - const changedIdentifiers = typeReferences.replaceWith((path) => { - // `Ref` - return j.tsTypeReference( - j.identifier("Ref"), - path.get("typeParameters").value, - ); - }); - if (changedIdentifiers.length > 0) { - hasChanges = true; - } - } - - const sourceIdentifierQualifiedNamesReferences = - findTSTypeReferenceCollections(j, ast, (node) => { - const { typeName } = node; - - return ( - typeName.type === "TSQualifiedName" && - typeName.right.type === "Identifier" && - typeName.right.name === "LegacyRef" - ); - }); - for (const typeReferences of sourceIdentifierQualifiedNamesReferences) { - const changedQualifiedNames = typeReferences.replaceWith((path) => { - const { node } = path; - const typeName = /** @type {import('jscodeshift').TSQualifiedName} */ ( - node.typeName - ); - // `*.Ref` - return j.tsTypeReference( - j.tsQualifiedName(typeName.left, j.identifier("Ref")), - path.get("typeParameters").value, - ); - }); - if (changedQualifiedNames.length > 0) { - hasChanges = true; - } - } + const hasChanges = renameType(j, ast, "LegacyRef", "Ref"); // Otherwise some files will be marked as "modified" because formatting changed if (hasChanges) { diff --git a/transforms/deprecated-react-child.js b/transforms/deprecated-react-child.js index 73cad285..78e56bb4 100644 --- a/transforms/deprecated-react-child.js +++ b/transforms/deprecated-react-child.js @@ -1,7 +1,5 @@ const parseSync = require("./utils/parseSync"); -const { - findTSTypeReferenceCollections, -} = require("./utils/jscodeshift-bugfixes"); +const { replaceType } = require("./utils/replaceType"); /** * @type {import('jscodeshift').Transform} @@ -10,51 +8,37 @@ const deprecatedReactChildTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - const reactChildTypeReferences = findTSTypeReferenceCollections( + const hasChanges = replaceType( j, ast, - (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; + "ReactChild", + (typeReference) => { + if (typeReference.typeName.type === "TSQualifiedName") { + return j.tsUnionType([ + // React.ReactElement + j.tsTypeReference( + j.tsQualifiedName( + j.identifier("React"), + j.identifier("ReactElement"), + ), + ), + j.tsNumberKeyword(), + j.tsStringKeyword(), + ]); + } else { + return j.tsUnionType([ + // React.ReactElement + j.tsTypeReference(j.identifier("ReactElement")), + j.tsNumberKeyword(), + j.tsStringKeyword(), + ]); } - - return identifier !== null && identifier.name === "ReactChild"; }, + "ReactElement", ); - let didChangeIdentifiers = false; - for (const typeReferences of reactChildTypeReferences) { - const changedIdentifiers = typeReferences.replaceWith(() => { - // `React.ReactElement | number | string` - return j.tsUnionType([ - // React.ReactElement - j.tsTypeReference( - j.tsQualifiedName( - j.identifier("React"), - j.identifier("ReactElement"), - ), - ), - j.tsNumberKeyword(), - j.tsStringKeyword(), - ]); - }); - if (changedIdentifiers.length > 0) { - didChangeIdentifiers = true; - } - } - // Otherwise some files will be marked as "modified" because formatting changed - if (didChangeIdentifiers) { + if (hasChanges) { return ast.toSource(); } return file.source; diff --git a/transforms/deprecated-react-fragment.js b/transforms/deprecated-react-fragment.js index d4548127..9f5abd8f 100644 --- a/transforms/deprecated-react-fragment.js +++ b/transforms/deprecated-react-fragment.js @@ -1,7 +1,6 @@ const parseSync = require("./utils/parseSync"); -const { - findTSTypeReferenceCollections, -} = require("./utils/jscodeshift-bugfixes"); +const { replaceType } = require("./utils/replaceType"); + /** * @type {import('jscodeshift').Transform} */ @@ -9,104 +8,36 @@ const deprecatedReactFragmentTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - let hasChanges = false; - - const hasReactNodeImport = ast.find(j.ImportSpecifier, (node) => { - const { imported, local } = node; - return ( - imported.type === "Identifier" && - imported.name === "ReactNode" && - // We don't support renames generally, so we don't handle them here - (local == null || local.name === "ReactNode") - ); - }); - const reactFragmentImports = ast.find(j.ImportSpecifier, (node) => { - const { imported, local } = node; - return ( - imported.type === "Identifier" && - imported.name === "ReactFragment" && - // We don't support renames generally, so we don't handle them here - (local == null || local.name === "ReactFragment") - ); - }); - if (reactFragmentImports.length > 0) { - hasChanges = true; - } - - if (hasReactNodeImport.length > 0) { - reactFragmentImports.remove(); - } else { - reactFragmentImports.replaceWith((path) => { - const importSpecifier = j.importSpecifier(j.identifier("ReactNode")); - const importDeclaration = path.parentPath.parentPath.value; - if (importDeclaration.importKind !== "type") { - // @ts-expect-error -- Missing types in jscodeshift. Babel uses `importKind`: https://astexplorer.net/#/gist/a76bd35f28483a467fef29d3c63aac9b/0e7ba6688fc09bd11b92197349b2384bb4c94574 - importSpecifier.importKind = "type"; - } - - return importSpecifier; - }); - } - - const reactFragmentTypeReferences = findTSTypeReferenceCollections( - j, - ast, - (node) => { - const { typeName } = node; - - return ( - typeName.type === "Identifier" && typeName.name === "ReactFragment" - ); - }, - ); - for (const typeReferences of reactFragmentTypeReferences) { - const changedIdentifiers = typeReferences.replaceWith(() => { - // `Iterable` - return j.tsTypeReference( - j.identifier("Iterable"), - j.tsTypeParameterInstantiation([ - j.tsTypeReference(j.identifier("ReactNode")), - ]), - ); - }); - if (changedIdentifiers.length > 0) { - hasChanges = true; - } - } - - const reactFragmentQualifiedNamesReferences = findTSTypeReferenceCollections( + const hasChanges = replaceType( j, ast, - (node) => { - const { typeName } = node; - - return ( - typeName.type === "TSQualifiedName" && - typeName.right.type === "Identifier" && - typeName.right.name === "ReactFragment" - ); + "ReactFragment", + (typeReference) => { + if (typeReference.typeName.type === "TSQualifiedName") { + // `Iterable<*.ReactNode>` + return j.tsTypeReference( + j.identifier("Iterable"), + j.tsTypeParameterInstantiation([ + j.tsTypeReference( + j.tsQualifiedName( + typeReference.typeName.left, + j.identifier("ReactNode"), + ), + ), + ]), + ); + } else { + // `Iterable` + return j.tsTypeReference( + j.identifier("Iterable"), + j.tsTypeParameterInstantiation([ + j.tsTypeReference(j.identifier("ReactNode")), + ]), + ); + } }, + "ReactNode", ); - for (const typeReferences of reactFragmentQualifiedNamesReferences) { - const changedQualifiedNames = typeReferences.replaceWith((path) => { - const { node } = path; - const typeName = /** @type {import('jscodeshift').TSQualifiedName} */ ( - node.typeName - ); - // `Iterable<*.ReactNode>` - return j.tsTypeReference( - j.identifier("Iterable"), - j.tsTypeParameterInstantiation([ - j.tsTypeReference( - j.tsQualifiedName(typeName.left, j.identifier("ReactNode")), - ), - ]), - ); - }); - if (changedQualifiedNames.length > 0) { - hasChanges = true; - } - } // Otherwise some files will be marked as "modified" because formatting changed if (hasChanges) { diff --git a/transforms/deprecated-react-node-array.js b/transforms/deprecated-react-node-array.js index 3d451468..aa7b74d3 100644 --- a/transforms/deprecated-react-node-array.js +++ b/transforms/deprecated-react-node-array.js @@ -1,7 +1,5 @@ const parseSync = require("./utils/parseSync"); -const { - findTSTypeReferenceCollections, -} = require("./utils/jscodeshift-bugfixes"); +const { replaceType } = require("./utils/replaceType"); /** * @type {import('jscodeshift').Transform} @@ -10,107 +8,36 @@ const deprecatedReactNodeArrayTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - let hasChanges = false; - - const hasReactNodeImport = ast.find(j.ImportSpecifier, (node) => { - const { imported, local } = node; - return ( - imported.type === "Identifier" && - imported.name === "ReactNode" && - // We don't support renames generally, so we don't handle them here - (local == null || local.name === "ReactNode") - ); - }); - const reactNodeArrayImports = ast.find(j.ImportSpecifier, (node) => { - const { imported, local } = node; - return ( - imported.type === "Identifier" && - imported.name === "ReactNodeArray" && - // We don't support renames generally, so we don't handle them here - (local == null || local.name === "ReactNodeArray") - ); - }); - if (reactNodeArrayImports.length > 0) { - hasChanges = true; - } - - if (hasReactNodeImport.length > 0) { - reactNodeArrayImports.remove(); - } else { - reactNodeArrayImports.replaceWith((path) => { - const importSpecifier = j.importSpecifier(j.identifier("ReactNode")); - - const importDeclaration = path.parentPath.parentPath.value; - if (importDeclaration.importKind !== "type") { - // @ts-expect-error -- Missing types in jscodeshift. Babel uses `importKind`: https://astexplorer.net/#/gist/a76bd35f28483a467fef29d3c63aac9b/0e7ba6688fc09bd11b92197349b2384bb4c94574 - importSpecifier.importKind = "type"; - } - - return importSpecifier; - }); - } - - const reactNodeArrayTypeReferences = findTSTypeReferenceCollections( - j, - ast, - (node) => { - const { typeName } = node; - - return ( - typeName.type === "Identifier" && typeName.name === "ReactNodeArray" - ); - }, - ); - for (const typeReferences of reactNodeArrayTypeReferences) { - const changedIdentifiers = typeReferences.replaceWith(() => { - // `ReadonlyArray` - return j.tsTypeReference( - j.identifier("ReadonlyArray"), - j.tsTypeParameterInstantiation([ - j.tsTypeReference(j.identifier("ReactNode")), - ]), - ); - }); - - if (changedIdentifiers.length > 0) { - hasChanges = true; - } - } - - const reactNodeArrayQualifiedTypeReferences = findTSTypeReferenceCollections( + const hasChanges = replaceType( j, ast, - (node) => { - const { typeName } = node; - - return ( - typeName.type === "TSQualifiedName" && - typeName.right.type === "Identifier" && - typeName.right.name === "ReactNodeArray" - ); + "ReactNodeArray", + (typeReference) => { + if (typeReference.typeName.type === "TSQualifiedName") { + // `ReadonlyArray<*.ReactNode>` + return j.tsTypeReference( + j.identifier("ReadonlyArray"), + j.tsTypeParameterInstantiation([ + j.tsTypeReference( + j.tsQualifiedName( + typeReference.typeName.left, + j.identifier("ReactNode"), + ), + ), + ]), + ); + } else { + // `ReadonlyArray` + return j.tsTypeReference( + j.identifier("ReadonlyArray"), + j.tsTypeParameterInstantiation([ + j.tsTypeReference(j.identifier("ReactNode")), + ]), + ); + } }, + "ReactNode", ); - for (const typeReferences of reactNodeArrayQualifiedTypeReferences) { - const changedQualifiedNames = typeReferences.replaceWith((path) => { - const { node } = path; - const typeName = /** @type {import('jscodeshift').TSQualifiedName} */ ( - node.typeName - ); - // `ReadonlyArray<*.ReactNode>` - return j.tsTypeReference( - j.identifier("ReadonlyArray"), - j.tsTypeParameterInstantiation([ - j.tsTypeReference( - j.tsQualifiedName(typeName.left, j.identifier("ReactNode")), - ), - ]), - ); - }); - - if (changedQualifiedNames.length > 0) { - hasChanges = true; - } - } // Otherwise some files will be marked as "modified" because formatting changed if (hasChanges) { diff --git a/transforms/deprecated-react-text.js b/transforms/deprecated-react-text.js index ebecec17..8439024c 100644 --- a/transforms/deprecated-react-text.js +++ b/transforms/deprecated-react-text.js @@ -1,7 +1,5 @@ const parseSync = require("./utils/parseSync"); -const { - findTSTypeReferenceCollections, -} = require("./utils/jscodeshift-bugfixes"); +const { replaceType } = require("./utils/replaceType"); /** * @type {import('jscodeshift').Transform} @@ -10,39 +8,16 @@ const deprecatedReactTextTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - let hasChanges = false; - - const reactTextTypeReferences = findTSTypeReferenceCollections( + const hasChanges = replaceType( j, ast, - (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 === "ReactText"; - }, - ); - - for (const typeReferences of reactTextTypeReferences) { - const changedIdentifiers = typeReferences.replaceWith(() => { + "ReactText", + () => { // `number | string` return j.tsUnionType([j.tsNumberKeyword(), j.tsStringKeyword()]); - }); - if (changedIdentifiers.length > 0) { - hasChanges = true; - } - } + }, + null, + ); // Otherwise some files will be marked as "modified" because formatting changed if (hasChanges) { diff --git a/transforms/deprecated-react-type.js b/transforms/deprecated-react-type.js index 38b7b0b1..45a058fe 100644 --- a/transforms/deprecated-react-type.js +++ b/transforms/deprecated-react-type.js @@ -1,4 +1,5 @@ const parseSync = require("./utils/parseSync"); +const { renameType } = require("./utils/replaceType"); /** * @type {import('jscodeshift').Transform} @@ -7,16 +8,10 @@ const deprecatedReactTypeTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - const changedIdentifiers = ast - .find(j.Identifier, (node) => { - return node.name === "ReactType"; - }) - .replaceWith(() => { - return j.identifier("ElementType"); - }); + const hasChanges = renameType(j, ast, "ReactType", "ElementType"); // Otherwise some files will be marked as "modified" because formatting changed - if (changedIdentifiers.length > 0) { + if (hasChanges) { return ast.toSource(); } return file.source; diff --git a/transforms/deprecated-sfc-element.js b/transforms/deprecated-sfc-element.js index 376a1347..2592154f 100644 --- a/transforms/deprecated-sfc-element.js +++ b/transforms/deprecated-sfc-element.js @@ -1,4 +1,5 @@ const parseSync = require("./utils/parseSync"); +const { renameType } = require("./utils/replaceType"); /** * @type {import('jscodeshift').Transform} @@ -7,16 +8,15 @@ const deprecatedSFCElementTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - const changedIdentifiers = ast - .find(j.Identifier, (node) => { - return node.name === "SFCElement"; - }) - .replaceWith(() => { - return j.identifier("FunctionComponentElement"); - }); + const hasChanges = renameType( + j, + ast, + "SFCElement", + "FunctionComponentElement", + ); // Otherwise some files will be marked as "modified" because formatting changed - if (changedIdentifiers.length > 0) { + if (hasChanges) { return ast.toSource(); } return file.source; diff --git a/transforms/deprecated-sfc.js b/transforms/deprecated-sfc.js index b1942bcc..aa6e97e8 100644 --- a/transforms/deprecated-sfc.js +++ b/transforms/deprecated-sfc.js @@ -1,4 +1,5 @@ const parseSync = require("./utils/parseSync"); +const { renameType } = require("./utils/replaceType"); /** * @type {import('jscodeshift').Transform} @@ -7,16 +8,10 @@ const deprecatedSFCTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - const changedIdentifiers = ast - .find(j.Identifier, (node) => { - return node.name === "SFC"; - }) - .replaceWith(() => { - return j.identifier("FC"); - }); + const hasChanges = renameType(j, ast, "SFC", "FC"); // Otherwise some files will be marked as "modified" because formatting changed - if (changedIdentifiers.length > 0) { + if (hasChanges) { return ast.toSource(); } return file.source; diff --git a/transforms/deprecated-stateless-component.js b/transforms/deprecated-stateless-component.js index ac1b59ba..3cf219c4 100644 --- a/transforms/deprecated-stateless-component.js +++ b/transforms/deprecated-stateless-component.js @@ -1,4 +1,5 @@ const parseSync = require("./utils/parseSync"); +const { renameType } = require("./utils/replaceType"); /** * @type {import('jscodeshift').Transform} @@ -7,16 +8,15 @@ const deprecatedStatelessComponentTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - const changedIdentifiers = ast - .find(j.Identifier, (node) => { - return node.name === "StatelessComponent"; - }) - .replaceWith(() => { - return j.identifier("FunctionComponent"); - }); + const hasChanges = renameType( + j, + ast, + "StatelessComponent", + "FunctionComponent", + ); // Otherwise some files will be marked as "modified" because formatting changed - if (changedIdentifiers.length > 0) { + if (hasChanges) { return ast.toSource(); } return file.source; diff --git a/transforms/deprecated-void-function-component.js b/transforms/deprecated-void-function-component.js index 1a611cd1..03f1459a 100644 --- a/transforms/deprecated-void-function-component.js +++ b/transforms/deprecated-void-function-component.js @@ -1,4 +1,5 @@ const parseSync = require("./utils/parseSync"); +const { renameType } = require("./utils/replaceType"); /** * @type {import('jscodeshift').Transform} @@ -7,18 +8,12 @@ const deprecatedVoidFunctionComponentTransform = (file, api) => { const j = api.jscodeshift; const ast = parseSync(file); - const changedIdentifiers = ast - .find(j.Identifier, (node) => { - return node.name === "VFC" || node.name === "VoidFunctionComponent"; - }) - .replaceWith((path) => { - return j.identifier( - path.node.name === "VFC" ? "FC" : "FunctionComponent", - ); - }); + const hasChanges = + renameType(j, ast, "VFC", "FC") || + renameType(j, ast, "VoidFunctionComponent", "FunctionComponent"); // Otherwise some files will be marked as "modified" because formatting changed - if (changedIdentifiers.length > 0) { + if (hasChanges) { return ast.toSource(); } return file.source; diff --git a/transforms/utils/replaceType.js b/transforms/utils/replaceType.js new file mode 100644 index 00000000..3250ab8d --- /dev/null +++ b/transforms/utils/replaceType.js @@ -0,0 +1,123 @@ +const { findTSTypeReferenceCollections } = require("./jscodeshift-bugfixes"); + +/** + * Transform that renames a type `sourceIdentifier` to `targetIdentifier`. + * This function will also rename imports and type references. + * It returns `true` if any changes were made. + * @param {import('jscodeshift').API['jscodeshift']} j + * @param {import('jscodeshift').Collection} ast + * @param {string} sourceIdentifier + * @param {string} targetIdentifier + * @returns {boolean} + */ +function renameType(j, ast, sourceIdentifier, targetIdentifier) { + return replaceType( + j, + ast, + sourceIdentifier, + (typeReference) => { + if (typeReference.typeName.type === "TSQualifiedName") { + // `*.TargetIdentifier` + return j.tsTypeReference( + j.tsQualifiedName( + typeReference.typeName.left, + j.identifier(targetIdentifier), + ), + typeReference.typeParameters, + ); + } else { + // `TargetIdentifier` + return j.tsTypeReference( + j.identifier(targetIdentifier), + typeReference.typeParameters, + ); + } + }, + targetIdentifier, + ); +} + +/** + * Transform that replaces a type reference to `sourceIdentifier` with a type + * constructed from `buildTargetTypeReference` preserving type parameters. + * It returns `true` if any changes were made. + * @param {import('jscodeshift').API['jscodeshift']} j + * @param {import('jscodeshift').Collection} ast + * @param {string} sourceIdentifier + * @param {(sourcePath: import("jscodeshift").TSTypeReference) => import("jscodeshift").TSTypeReference | import("jscodeshift").TSUnionType} buildTargetTypeReference + * @param {string | null} addedType - `null` if no type was added + */ +function replaceType( + j, + ast, + sourceIdentifier, + buildTargetTypeReference, + addedType, +) { + let hasChanges = false; + + const targetIdentifierImports = ast.find(j.ImportSpecifier, (node) => { + const { imported, local } = node; + return ( + addedType !== null && + imported.type === "Identifier" && + imported.name === addedType && + // We don't support renames generally, so we don't handle them here + (local == null || local.name === addedType) + ); + }); + const sourceIdentifierImports = ast.find(j.ImportSpecifier, (node) => { + const { imported, local } = node; + return ( + imported.type === "Identifier" && + imported.name === sourceIdentifier && + // We don't support renames generally, so we don't handle them here + (local == null || local.name === sourceIdentifier) + ); + }); + if (sourceIdentifierImports.length > 0) { + hasChanges = true; + + if (addedType === null || targetIdentifierImports.length > 0) { + sourceIdentifierImports.remove(); + } else { + sourceIdentifierImports.replaceWith((path) => { + const importSpecifier = j.importSpecifier(j.identifier(addedType)); + if ("importKind" in path.node) { + // @ts-expect-error -- Missing types in jscodeshift. Babel uses `importKind`: https://astexplorer.net/#/gist/a76bd35f28483a467fef29d3c63aac9b/0e7ba6688fc09bd11b92197349b2384bb4c94574 + importSpecifier.importKind = path.node.importKind; + } + + return importSpecifier; + }); + } + } + + const sourceIdentifierTypeReferences = findTSTypeReferenceCollections( + j, + ast, + (node) => { + const { typeName } = node; + + return ( + (typeName.type === "Identifier" && + typeName.name === sourceIdentifier) || + (typeName.type === "TSQualifiedName" && + typeName.right.type === "Identifier" && + typeName.right.name === sourceIdentifier) + ); + }, + ); + for (const typeReferences of sourceIdentifierTypeReferences) { + const changedIdentifiers = typeReferences.replaceWith((path) => { + return buildTargetTypeReference(path.value); + }); + if (changedIdentifiers.length > 0) { + hasChanges = true; + } + } + + return hasChanges; +} + +module.exports = { replaceType, renameType };