Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 9 additions & 1 deletion src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
115 changes: 110 additions & 5 deletions src/transforms/react-types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,61 @@
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;
}
} else if (
n.local?.name === "ChildrenArray" &&
n.imported?.name === "ChildrenArray"
) {
return false;
} else {
return true;
}
});
}
},
};

export const GenericTypeAnnotation = {
exit(path, state) {
const { id: typeName, typeParameters } = path.node;
Expand All @@ -12,7 +68,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])
);
}

Expand Down Expand Up @@ -81,6 +148,36 @@ 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")
) {
// ElementConfig<T> -> JSX.LibraryManagedAttributes<T, ComponentProps<T>>
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;

Expand All @@ -106,8 +203,18 @@ 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]),
]);
}
}
}
},
};

// Mapping between React types for Flow and those for TypeScript.
Expand Down Expand Up @@ -144,17 +251,15 @@ export const QualifiedTypeIdentifier = {
t.identifier(QualifiedReactTypeNameMap[right.name])
);
}
}
},
};

// Only types with different names are included.
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",

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @flow
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<T>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"prettier": true,
"prettierOptions": {
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
ReactNode,
ReactText,
ReactChild,
ReactPortal,
ReactNodeArray,
ReactElement,
} from "react";
let node: ReactNode;
let text: ReactText;
let child: ReactChild;
let portal: ReactPortal;
let nodeArray: ReactNodeArray;
let element: ReactElement;
let children: T | T[];
3 changes: 1 addition & 2 deletions test/fixtures/convert/react/basic-types-different/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ 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;
let children: React.ChildrenArray<T>;
3 changes: 1 addition & 2 deletions test/fixtures/convert/react/basic-types-different/ts.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ 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;
let children: T | T[];
2 changes: 2 additions & 0 deletions test/fixtures/convert/react/basic-types-same/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 2 additions & 0 deletions test/fixtures/convert/react/basic-types-same/ts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
17 changes: 17 additions & 0 deletions test/fixtures/convert/react/colliding-types-unqualified/flow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @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;
let chilernArray: ChildrenArray;
16 changes: 16 additions & 0 deletions test/fixtures/convert/react/colliding-types-unqualified/ts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
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;
let chilernArray: ChildrenArray;
Original file line number Diff line number Diff line change
@@ -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<T>;
3 changes: 3 additions & 0 deletions test/fixtures/convert/react/element-config-unqualified/ts.js
Original file line number Diff line number Diff line change
@@ -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<T, ComponentProps<T>>;