Skip to content
Merged
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,32 @@ const WithIntersection: React.FC<Props1 & Props2> = ({ id, ...restProps }) => <s
const WithIntersection = ( { id, ...restProps }: Props1 & Props2 ) => <span>{id}</span>
```

and with component modules defined using intersection

```tsx
// before codemod runs
import React from 'react';
import { OtherComponent } from "./other-component";

interface Props { text: string }
const WithComponentIntersection: React.FC<Props> & {
OtherComponent: typeof OtherComponent;
} = (props) => {
return <span>{props.text}</span>
}
WithComponentIntersection.OtherComponent = OtherComponent;

// after codemod runs
import React from 'react';
import { OtherComponent } from "./other-component";

interface Props { text: string }
const WithComponentIntersection = (props: Props) => {
return <span>{props.text}</span>
}
WithComponentIntersection.OtherComponent = OtherComponent;
```

Even with no Props!

```tsx
Expand Down
21 changes: 18 additions & 3 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,21 @@ const isTsIntersectionType = (x) => x.type === 'TSIntersectionType';
const isArrowFunctionExpression = (x) => x.type === 'ArrowFunctionExpression';
// Using a function that accepts a component definition
const isCallExpression = (x) => x?.type === 'CallExpression';
const isTSIntersectionType = (x) => x?.type === 'TSIntersectionType';
exports.default = (fileInfo, { j }) => {
function addPropsTypeToComponentBody(n) {
// extract the Prop's type text
const reactFcOrSfcNode = n.node.id.typeAnnotation.typeAnnotation;
let reactFcOrSfcNode;
if (isIdentifier(n.node.id)) {
if (isTSIntersectionType(n.node.id.typeAnnotation.typeAnnotation)) {
reactFcOrSfcNode = n.node.id.typeAnnotation.typeAnnotation.types[0];
}
else {
reactFcOrSfcNode = n.node.id.typeAnnotation.typeAnnotation;
}
}
// shape of React.FC (no props)
if (!reactFcOrSfcNode.typeParameters) {
if (!reactFcOrSfcNode?.typeParameters) {
return;
}
const outerNewTypeAnnotation = extractPropsDefinitionFromReactFC(j, reactFcOrSfcNode);
Expand Down Expand Up @@ -111,7 +120,13 @@ exports.default = (fileInfo, { j }) => {
const newSource = root
.find(j.VariableDeclarator, (n) => {
const identifier = n?.id;
const typeName = identifier?.typeAnnotation?.typeAnnotation?.typeName;
let typeName;
if (isTSIntersectionType(identifier?.typeAnnotation?.typeAnnotation)) {
typeName = identifier.typeAnnotation.typeAnnotation.types[0].typeName;
}
else {
typeName = identifier?.typeAnnotation?.typeAnnotation?.typeName;
}
const genericParamsType = identifier?.typeAnnotation?.typeAnnotation?.typeParameters?.type;
// verify it is the shape of React.FC<Props> React.SFC<Props>, React.FC<{ type: string }>, FC<Props>, SFC<Props>, and so on
const isEqualFcOrFunctionComponent = (name) => ['FC', 'FunctionComponent'].includes(name);
Expand Down
121 changes: 121 additions & 0 deletions transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,127 @@ const testCases: TestCase[] = [
return <span>{id}</span>
})`,
},
{
input: `
import React from 'react'
import { OtherComponent } from "./other-component";

export const MyComponent: {
(): JSX.Element;
OtherComponent: typeof OtherComponent;
} = () => <span>foo</span>;
MyComponent.OtherComponent = OtherComponent;
`,
output: null,
},
{
input: `
import React from 'react'
import { OtherComponent } from "./other-component";
interface Props {
text: string;
}
export const MyComponent: React.FC<Props> & {
OtherComponent: typeof OtherComponent;
} = (props) => <span>{props.text}</span>;
MyComponent.OtherComponent = OtherComponent;
`,
output: `
import React from 'react'
import { OtherComponent } from "./other-component";
interface Props {
text: string;
}
export const MyComponent = (props: Props) => <span>{props.text}</span>;
MyComponent.OtherComponent = OtherComponent;
`,
},
{
input: `
import React from 'react'
import { OtherComponent } from "./other-component";

type Props = { id: number };
export const MyComponent: React.FC<Props> & {
OtherComponent: typeof OtherComponent;
} = ({ text }) => <span>{text}</span>;
MyComponent.OtherComponent = OtherComponent;
`,
output: `
import React from 'react'
import { OtherComponent } from "./other-component";

type Props = { id: number };
export const MyComponent = ( { text }: Props ) => <span>{text}</span>;
MyComponent.OtherComponent = OtherComponent;
`,
},
{
input: `
import React from 'react'
import { OtherComponent } from "./other-component";

type Props = { id: number };
export const MyComponent: React.FunctionComponent<Props> & {
OtherComponent: typeof OtherComponent;
} = (props) => <span>{props.text}</span>;
MyComponent.OtherComponent = OtherComponent;
`,
output: `
import React from 'react'
import { OtherComponent } from "./other-component";

type Props = { id: number };
export const MyComponent = (props: Props) => <span>{props.text}</span>;
MyComponent.OtherComponent = OtherComponent;
`,
},
{
input: `
import React from 'react'
import { OtherComponent } from "./other-component";

type Props = { id: number };
export const MyComponent: React.SFC<Props> & {
OtherComponent: typeof OtherComponent;
} = ({ text }) => <span>{text}</span>;
MyComponent.OtherComponent = OtherComponent;
`,
output: `
import React from 'react'
import { OtherComponent } from "./other-component";

type Props = { id: number };
export const MyComponent = ( { text }: Props ) => <span>{text}</span>;
MyComponent.OtherComponent = OtherComponent;
`,
},
{
input: `
import React from 'react';
import { OtherComponent } from "./other-component";
import { observer } from "mobx-react-lite";

type Props = { id: number };
const MyComponent: React.FC<Props> & {
OtherComponent: typeof OtherComponent;
} = observer((props) => {
return <span>{props.id}</span>
})
MyComponent.OtherComponent = OtherComponent;
`,
output: `
import React from 'react';
import { OtherComponent } from "./other-component";
import { observer } from "mobx-react-lite";

type Props = { id: number };
const MyComponent = observer((props: Props) => {
return <span>{props.id}</span>
})
MyComponent.OtherComponent = OtherComponent;
`,
},
]

function escapeLineEndingsAndMultiWhiteSpaces(text: string | null | undefined) {
Expand Down
21 changes: 18 additions & 3 deletions transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,22 @@ const isArrowFunctionExpression = (x: any): x is ArrowFunctionExpression =>
(x as ArrowFunctionExpression).type === 'ArrowFunctionExpression'
// Using a function that accepts a component definition
const isCallExpression = (x: any): x is CallExpression => x?.type === 'CallExpression'
const isTSIntersectionType = (x: any): x is TSIntersectionType => x?.type === 'TSIntersectionType'

export default (fileInfo: FileInfo, { j }: API) => {
function addPropsTypeToComponentBody(n: ASTPath<VariableDeclarator>) {
// extract the Prop's type text
const reactFcOrSfcNode = (n.node.id as Identifier).typeAnnotation!.typeAnnotation as TSTypeReference
let reactFcOrSfcNode
if (isIdentifier(n.node.id)) {
if (isTSIntersectionType(n.node.id.typeAnnotation!.typeAnnotation)) {
reactFcOrSfcNode = n.node.id.typeAnnotation!.typeAnnotation.types[0] as TSTypeReference
} else {
reactFcOrSfcNode = n.node.id.typeAnnotation!.typeAnnotation as TSTypeReference
}
}

// shape of React.FC (no props)
if (!reactFcOrSfcNode.typeParameters) {
if (!reactFcOrSfcNode?.typeParameters) {
return
}

Expand Down Expand Up @@ -129,7 +138,13 @@ export default (fileInfo: FileInfo, { j }: API) => {
const newSource = root
.find(j.VariableDeclarator, (n: any) => {
const identifier = n?.id
const typeName = identifier?.typeAnnotation?.typeAnnotation?.typeName
let typeName
if (isTSIntersectionType(identifier?.typeAnnotation?.typeAnnotation)) {
typeName = identifier.typeAnnotation.typeAnnotation.types[0].typeName
} else {
typeName = identifier?.typeAnnotation?.typeAnnotation?.typeName
}

const genericParamsType = identifier?.typeAnnotation?.typeAnnotation?.typeParameters?.type
// verify it is the shape of React.FC<Props> React.SFC<Props>, React.FC<{ type: string }>, FC<Props>, SFC<Props>, and so on

Expand Down