From c6f0bd151ab4bf8cfaea07f9fe6ba96638164d74 Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Wed, 24 Jan 2024 10:09:51 -0800 Subject: [PATCH 1/4] Add tests. --- .../basic-types-different-unqualified/flow.js | 19 +++++++++++++++++++ .../options.json | 9 +++++++++ .../basic-types-different-unqualified/ts.js | 18 ++++++++++++++++++ .../react/colliding-types-unqualified/flow.js | 16 ++++++++++++++++ .../react/colliding-types-unqualified/ts.js | 15 +++++++++++++++ .../react/element-config-unqualified/flow.js | 4 ++++ .../react/element-config-unqualified/ts.js | 3 +++ 7 files changed, 84 insertions(+) create mode 100644 test/fixtures/convert/react/basic-types-different-unqualified/flow.js create mode 100644 test/fixtures/convert/react/basic-types-different-unqualified/options.json create mode 100644 test/fixtures/convert/react/basic-types-different-unqualified/ts.js create mode 100644 test/fixtures/convert/react/colliding-types-unqualified/flow.js create mode 100644 test/fixtures/convert/react/colliding-types-unqualified/ts.js create mode 100644 test/fixtures/convert/react/element-config-unqualified/flow.js create mode 100644 test/fixtures/convert/react/element-config-unqualified/ts.js diff --git a/test/fixtures/convert/react/basic-types-different-unqualified/flow.js b/test/fixtures/convert/react/basic-types-different-unqualified/flow.js new file mode 100644 index 00000000..10972cc3 --- /dev/null +++ b/test/fixtures/convert/react/basic-types-different-unqualified/flow.js @@ -0,0 +1,19 @@ +// @flow +import { + Node, + Text, + Child, + Children, + Fragment, + Portal, + NodeArray, + Element, +} from "react"; +let node: Node; +let text: Text; +let child: Child; +let children: Children; +let fragment: Fragment; +let portal: Portal; +let nodeArray: NodeArray; +let element: Element; diff --git a/test/fixtures/convert/react/basic-types-different-unqualified/options.json b/test/fixtures/convert/react/basic-types-different-unqualified/options.json new file mode 100644 index 00000000..5df2eb2e --- /dev/null +++ b/test/fixtures/convert/react/basic-types-different-unqualified/options.json @@ -0,0 +1,9 @@ +{ + "prettier": true, + "prettierOptions": { + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "all" + } +} \ No newline at end of file diff --git a/test/fixtures/convert/react/basic-types-different-unqualified/ts.js b/test/fixtures/convert/react/basic-types-different-unqualified/ts.js new file mode 100644 index 00000000..22207640 --- /dev/null +++ b/test/fixtures/convert/react/basic-types-different-unqualified/ts.js @@ -0,0 +1,18 @@ +import { + ReactNode, + ReactText, + ReactChild, + ReactChildren, + ReactFragment, + ReactPortal, + ReactNodeArray, + ReactElement, +} from "react"; +let node: ReactNode; +let text: ReactText; +let child: ReactChild; +let children: ReactChildren; +let fragment: ReactFragment; +let portal: ReactPortal; +let nodeArray: ReactNodeArray; +let element: ReactElement; diff --git a/test/fixtures/convert/react/colliding-types-unqualified/flow.js b/test/fixtures/convert/react/colliding-types-unqualified/flow.js new file mode 100644 index 00000000..0372976f --- /dev/null +++ b/test/fixtures/convert/react/colliding-types-unqualified/flow.js @@ -0,0 +1,16 @@ +// @flow +let node: Node; +let text: Text; +let child: Child; +let children: Children; +let fragment: Fragment; +let portal: Portal; +let nodeArray: NodeArray; +let element: Element; +let component: Component; +let pureComponent: PureComponent; +let componentType: ComponentType; +let context: Context; +let ref: Ref; +let key: Key; +let elementConfig: ElementConfig; diff --git a/test/fixtures/convert/react/colliding-types-unqualified/ts.js b/test/fixtures/convert/react/colliding-types-unqualified/ts.js new file mode 100644 index 00000000..ce268a9d --- /dev/null +++ b/test/fixtures/convert/react/colliding-types-unqualified/ts.js @@ -0,0 +1,15 @@ +let node: Node; +let text: Text; +let child: Child; +let children: Children; +let fragment: Fragment; +let portal: Portal; +let nodeArray: NodeArray; +let element: Element; +let component: Component; +let pureComponent: PureComponent; +let componentType: ComponentType; +let context: Context; +let ref: Ref; +let key: Key; +let elementConfig: ElementConfig; diff --git a/test/fixtures/convert/react/element-config-unqualified/flow.js b/test/fixtures/convert/react/element-config-unqualified/flow.js new file mode 100644 index 00000000..781fbe4c --- /dev/null +++ b/test/fixtures/convert/react/element-config-unqualified/flow.js @@ -0,0 +1,4 @@ +// @flow +// Include an unused reference to ComponentProps to ensure we don't import it twice. +import { ElementConfig, ComponentProps } from "react"; +type Props = ElementConfig; diff --git a/test/fixtures/convert/react/element-config-unqualified/ts.js b/test/fixtures/convert/react/element-config-unqualified/ts.js new file mode 100644 index 00000000..603c4e15 --- /dev/null +++ b/test/fixtures/convert/react/element-config-unqualified/ts.js @@ -0,0 +1,3 @@ +// Include an unused reference to ComponentProps to ensure we don't import it twice. +import { ComponentProps } from "react"; +type Props = JSX.LibraryManagedAttributes>; From 4cddf9e57c7dc42336859f722d227bbcee528dbd Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Wed, 24 Jan 2024 11:24:43 -0800 Subject: [PATCH 2/4] Support rewriting unqualified React imports. --- src/convert.ts | 1 + src/transform.ts | 10 +++- src/transforms/react-types.ts | 87 +++++++++++++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/convert.ts b/src/convert.ts index fc5bf05c..fe779984 100644 --- a/src/convert.ts +++ b/src/convert.ts @@ -67,6 +67,7 @@ export const convert = (flowCode: string, options?: any) => { // apply our transforms, traverse mutates the ast const state = { usedUtilityTypes: new Set(), + unqualifiedReactImports: new Set(), options: Object.assign({ inlineUtilityTypes: false }, options), commentsToNodesMap, startLineToComments, diff --git a/src/transform.ts b/src/transform.ts index c6b8712f..95a43c06 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -660,14 +660,22 @@ export const transform = { path.replaceWith(replacementNode); } } + + reactTypes.ImportDeclaration.exit(path, state); }, }, ImportSpecifier: { - exit(path) { + exit(path, state) { // TODO(#223): Handle "typeof" imports. if (path.node.importKind === "typeof") { path.node.importKind = "value"; } + + const replacement = reactTypes.ImportSpecifier.exit(path, state); + if (replacement) { + path.replaceWith(replacement); + return; + } }, }, DeclareVariable: declare.DeclareVariable, diff --git a/src/transforms/react-types.ts b/src/transforms/react-types.ts index 19cce3b3..ff61a085 100644 --- a/src/transforms/react-types.ts +++ b/src/transforms/react-types.ts @@ -1,5 +1,55 @@ import * as t from "@babel/types"; +export const ImportSpecifier = { + exit(path, state) { + const { local, imported } = path.node; + + if ( + path.parent.source.value === "react" && + // TODO: Support transforming unqualified React types imported as aliases. + local.name === imported.name + ) { + state.unqualifiedReactImports.add(local.name); + + if (local.name in QualifiedReactTypeNameMap) { + return t.importSpecifier( + t.identifier(QualifiedReactTypeNameMap[local.name]), + t.identifier(QualifiedReactTypeNameMap[local.name]) + ); + } + + if (local.name === "ElementConfig") { + return t.importSpecifier( + t.identifier("ComponentProps"), + t.identifier("ComponentProps") + ); + } + } + }, +}; + +export const ImportDeclaration = { + exit(path, state) { + if (path.node?.source?.value === "react") { + let seenComponentProps = false; + path.node.specifiers = (path.node.specifiers ?? []).filter((n) => { + if ( + n.local?.name === "ComponentProps" && + n.imported?.name === "ComponentProps" + ) { + if (!seenComponentProps) { + seenComponentProps = true; + return true; + } else { + return false; + } + } + return true; + }); + } + }, +}; + export const GenericTypeAnnotation = { exit(path, state) { const { id: typeName, typeParameters } = path.node; @@ -12,7 +62,18 @@ export const GenericTypeAnnotation = { t.identifier(UnqualifiedReactTypeNameMap[typeName.name]) ), // TypeScript doesn't support empty type param lists - typeParameters && typeParameters.params.length > 0 ? typeParameters : null + typeParameters && typeParameters.params.length > 0 + ? typeParameters + : null + ); + } + + if ( + typeName.name in QualifiedReactTypeNameMap && + state.unqualifiedReactImports.has(typeName.name) + ) { + return t.tsTypeReference( + t.identifier(QualifiedReactTypeNameMap[typeName.name]) ); } @@ -81,6 +142,26 @@ export const GenericTypeAnnotation = { ); } + if ( + typeName.name === "ElementConfig" && + state.unqualifiedReactImports.has("ElementConfig") + ) { + // ElementConfig -> JSX.LibraryManagedAttributes> + return t.tsTypeReference( + t.tsQualifiedName( + t.identifier("JSX"), + t.identifier("LibraryManagedAttributes") + ), + t.tsTypeParameterInstantiation([ + typeParameters.params[0], + t.tsTypeReference( + t.identifier("ComponentProps"), + t.tsTypeParameterInstantiation([typeParameters.params[0]]) + ), + ]) + ); + } + if (t.isTSQualifiedName(typeName)) { const { left, right } = typeName; @@ -107,7 +188,7 @@ export const GenericTypeAnnotation = { ); } } - } + }, }; // Mapping between React types for Flow and those for TypeScript. @@ -144,7 +225,7 @@ export const QualifiedTypeIdentifier = { t.identifier(QualifiedReactTypeNameMap[right.name]) ); } - } + }, }; // Only types with different names are included. From d0d9cadefdad3f1bae7c4070f18e632d35d82e72 Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Fri, 26 Jan 2024 11:45:17 -0800 Subject: [PATCH 3/4] Children and Fragment are now the same across Flow and TypeScript. --- src/transforms/react-types.ts | 2 -- .../react/basic-types-different-unqualified/flow.js | 13 +------------ .../react/basic-types-different-unqualified/ts.js | 4 ---- .../convert/react/basic-types-different/flow.js | 2 -- .../convert/react/basic-types-different/ts.js | 2 -- .../fixtures/convert/react/basic-types-same/flow.js | 2 ++ test/fixtures/convert/react/basic-types-same/ts.js | 2 ++ 7 files changed, 5 insertions(+), 22 deletions(-) diff --git a/src/transforms/react-types.ts b/src/transforms/react-types.ts index ff61a085..6354d0cd 100644 --- a/src/transforms/react-types.ts +++ b/src/transforms/react-types.ts @@ -233,9 +233,7 @@ const QualifiedReactTypeNameMap = { Node: "ReactNode", Text: "ReactText", Child: "ReactChild", - Children: "ReactChildren", Element: "ReactElement", // 1:1 mapping is wrong, since ReactElement takes two type params - Fragment: "ReactFragment", Portal: "ReactPortal", NodeArray: "ReactNodeArray", diff --git a/test/fixtures/convert/react/basic-types-different-unqualified/flow.js b/test/fixtures/convert/react/basic-types-different-unqualified/flow.js index 10972cc3..9e6cb16c 100644 --- a/test/fixtures/convert/react/basic-types-different-unqualified/flow.js +++ b/test/fixtures/convert/react/basic-types-different-unqualified/flow.js @@ -1,19 +1,8 @@ // @flow -import { - Node, - Text, - Child, - Children, - Fragment, - Portal, - NodeArray, - Element, -} from "react"; +import { Node, Text, Child, Portal, NodeArray, Element } from "react"; let node: Node; let text: Text; let child: Child; -let children: Children; -let fragment: Fragment; let portal: Portal; let nodeArray: NodeArray; let element: Element; diff --git a/test/fixtures/convert/react/basic-types-different-unqualified/ts.js b/test/fixtures/convert/react/basic-types-different-unqualified/ts.js index 22207640..4fa76df3 100644 --- a/test/fixtures/convert/react/basic-types-different-unqualified/ts.js +++ b/test/fixtures/convert/react/basic-types-different-unqualified/ts.js @@ -2,8 +2,6 @@ import { ReactNode, ReactText, ReactChild, - ReactChildren, - ReactFragment, ReactPortal, ReactNodeArray, ReactElement, @@ -11,8 +9,6 @@ import { let node: ReactNode; let text: ReactText; let child: ReactChild; -let children: ReactChildren; -let fragment: ReactFragment; let portal: ReactPortal; let nodeArray: ReactNodeArray; let element: ReactElement; diff --git a/test/fixtures/convert/react/basic-types-different/flow.js b/test/fixtures/convert/react/basic-types-different/flow.js index d2f54dbb..780c697b 100644 --- a/test/fixtures/convert/react/basic-types-different/flow.js +++ b/test/fixtures/convert/react/basic-types-different/flow.js @@ -3,8 +3,6 @@ import * as React from "react"; let node: React.Node; let text: React.Text; let child: React.Child; -let children: React.Children; -let fragment: React.Fragment; let portal: React.Portal; let nodeArray: React.NodeArray; let element: React.Element; diff --git a/test/fixtures/convert/react/basic-types-different/ts.js b/test/fixtures/convert/react/basic-types-different/ts.js index f44ec3e8..f3504894 100644 --- a/test/fixtures/convert/react/basic-types-different/ts.js +++ b/test/fixtures/convert/react/basic-types-different/ts.js @@ -2,8 +2,6 @@ import * as React from "react"; let node: React.ReactNode; let text: React.ReactText; let child: React.ReactChild; -let children: React.ReactChildren; -let fragment: React.ReactFragment; let portal: React.ReactPortal; let nodeArray: React.ReactNodeArray; let element: React.ReactElement; diff --git a/test/fixtures/convert/react/basic-types-same/flow.js b/test/fixtures/convert/react/basic-types-same/flow.js index 2b91f972..45bec749 100644 --- a/test/fixtures/convert/react/basic-types-same/flow.js +++ b/test/fixtures/convert/react/basic-types-same/flow.js @@ -6,3 +6,5 @@ let componentType: React.ComponentType; let context: React.Context; let ref: React.Ref; let key: React.Key; +let children: React.Children; +let fragment: React.Fragment; diff --git a/test/fixtures/convert/react/basic-types-same/ts.js b/test/fixtures/convert/react/basic-types-same/ts.js index 7c13bdcb..0509b481 100644 --- a/test/fixtures/convert/react/basic-types-same/ts.js +++ b/test/fixtures/convert/react/basic-types-same/ts.js @@ -5,3 +5,5 @@ let componentType: React.ComponentType; let context: React.Context; let ref: React.Ref; let key: React.Key; +let children: React.Children; +let fragment: React.Fragment; From dda9eb4bb902b8a76425ddfb6b51597e9a52bef1 Mon Sep 17 00:00:00 2001 From: Sean Kelley Date: Fri, 26 Jan 2024 11:55:46 -0800 Subject: [PATCH 4/4] Convert ChildrenArray to T | T[], which mimics the type signatures of the Children utility methods. --- src/transforms/react-types.ts | 28 ++++++++++++++++++- .../basic-types-different-unqualified/flow.js | 11 +++++++- .../basic-types-different-unqualified/ts.js | 1 + .../react/basic-types-different/flow.js | 1 + .../convert/react/basic-types-different/ts.js | 1 + .../react/colliding-types-unqualified/flow.js | 1 + .../react/colliding-types-unqualified/ts.js | 1 + 7 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/transforms/react-types.ts b/src/transforms/react-types.ts index 6354d0cd..98fef13b 100644 --- a/src/transforms/react-types.ts +++ b/src/transforms/react-types.ts @@ -43,8 +43,14 @@ export const ImportDeclaration = { } else { return false; } + } else if ( + n.local?.name === "ChildrenArray" && + n.imported?.name === "ChildrenArray" + ) { + return false; + } else { + return true; } - return true; }); } }, @@ -142,6 +148,16 @@ export const GenericTypeAnnotation = { ); } + if ( + typeName.name === "ChildrenArray" && + state.unqualifiedReactImports.has("ChildrenArray") + ) { + return t.tsUnionType([ + typeParameters.params[0], + t.tsArrayType(typeParameters.params[0]), + ]); + } + if ( typeName.name === "ElementConfig" && state.unqualifiedReactImports.has("ElementConfig") @@ -187,6 +203,16 @@ export const GenericTypeAnnotation = { ]) ); } + + if ( + t.isIdentifier(left, { name: "React" }) && + t.isIdentifier(right, { name: "ChildrenArray" }) + ) { + return t.tsUnionType([ + typeParameters.params[0], + t.tsArrayType(typeParameters.params[0]), + ]); + } } }, }; diff --git a/test/fixtures/convert/react/basic-types-different-unqualified/flow.js b/test/fixtures/convert/react/basic-types-different-unqualified/flow.js index 9e6cb16c..59817935 100644 --- a/test/fixtures/convert/react/basic-types-different-unqualified/flow.js +++ b/test/fixtures/convert/react/basic-types-different-unqualified/flow.js @@ -1,8 +1,17 @@ // @flow -import { Node, Text, Child, Portal, NodeArray, Element } from "react"; +import { + Node, + Text, + Child, + Portal, + NodeArray, + Element, + ChildrenArray, +} from "react"; let node: Node; let text: Text; let child: Child; let portal: Portal; let nodeArray: NodeArray; let element: Element; +let children: ChildrenArray; diff --git a/test/fixtures/convert/react/basic-types-different-unqualified/ts.js b/test/fixtures/convert/react/basic-types-different-unqualified/ts.js index 4fa76df3..7ee4a3b6 100644 --- a/test/fixtures/convert/react/basic-types-different-unqualified/ts.js +++ b/test/fixtures/convert/react/basic-types-different-unqualified/ts.js @@ -12,3 +12,4 @@ let child: ReactChild; let portal: ReactPortal; let nodeArray: ReactNodeArray; let element: ReactElement; +let children: T | T[]; diff --git a/test/fixtures/convert/react/basic-types-different/flow.js b/test/fixtures/convert/react/basic-types-different/flow.js index 780c697b..d3dcd48f 100644 --- a/test/fixtures/convert/react/basic-types-different/flow.js +++ b/test/fixtures/convert/react/basic-types-different/flow.js @@ -6,3 +6,4 @@ let child: React.Child; let portal: React.Portal; let nodeArray: React.NodeArray; let element: React.Element; +let children: React.ChildrenArray; diff --git a/test/fixtures/convert/react/basic-types-different/ts.js b/test/fixtures/convert/react/basic-types-different/ts.js index f3504894..8db73846 100644 --- a/test/fixtures/convert/react/basic-types-different/ts.js +++ b/test/fixtures/convert/react/basic-types-different/ts.js @@ -5,3 +5,4 @@ let child: React.ReactChild; let portal: React.ReactPortal; let nodeArray: React.ReactNodeArray; let element: React.ReactElement; +let children: T | T[]; diff --git a/test/fixtures/convert/react/colliding-types-unqualified/flow.js b/test/fixtures/convert/react/colliding-types-unqualified/flow.js index 0372976f..5b06119a 100644 --- a/test/fixtures/convert/react/colliding-types-unqualified/flow.js +++ b/test/fixtures/convert/react/colliding-types-unqualified/flow.js @@ -14,3 +14,4 @@ let context: Context; let ref: Ref; let key: Key; let elementConfig: ElementConfig; +let chilernArray: ChildrenArray; diff --git a/test/fixtures/convert/react/colliding-types-unqualified/ts.js b/test/fixtures/convert/react/colliding-types-unqualified/ts.js index ce268a9d..2c6a31eb 100644 --- a/test/fixtures/convert/react/colliding-types-unqualified/ts.js +++ b/test/fixtures/convert/react/colliding-types-unqualified/ts.js @@ -13,3 +13,4 @@ let context: Context; let ref: Ref; let key: Key; let elementConfig: ElementConfig; +let chilernArray: ChildrenArray;