diff --git a/README.md b/README.md index 91d46cc..3587398 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,32 @@ const WithIntersection: React.FC = ({ id, ...restProps }) => {id} ``` +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 & { + OtherComponent: typeof OtherComponent; +} = (props) => { + return {props.text} +} +WithComponentIntersection.OtherComponent = OtherComponent; + +// after codemod runs +import React from 'react'; +import { OtherComponent } from "./other-component"; + +interface Props { text: string } +const WithComponentIntersection = (props: Props) => { + return {props.text} +} +WithComponentIntersection.OtherComponent = OtherComponent; +``` + Even with no Props! ```tsx diff --git a/dist/index.js b/dist/index.js index 4af23cc..7de4a31 100644 --- a/dist/index.js +++ b/dist/index.js @@ -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); @@ -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 React.SFC, React.FC<{ type: string }>, FC, SFC, and so on const isEqualFcOrFunctionComponent = (name) => ['FC', 'FunctionComponent'].includes(name); diff --git a/transform.test.ts b/transform.test.ts index 7739a59..08b696d 100644 --- a/transform.test.ts +++ b/transform.test.ts @@ -738,6 +738,127 @@ const testCases: TestCase[] = [ return {id} })`, }, + { + input: ` + import React from 'react' + import { OtherComponent } from "./other-component"; + + export const MyComponent: { + (): JSX.Element; + OtherComponent: typeof OtherComponent; + } = () => foo; + MyComponent.OtherComponent = OtherComponent; + `, + output: null, + }, + { + input: ` + import React from 'react' + import { OtherComponent } from "./other-component"; + interface Props { + text: string; + } + export const MyComponent: React.FC & { + OtherComponent: typeof OtherComponent; + } = (props) => {props.text}; + MyComponent.OtherComponent = OtherComponent; + `, + output: ` + import React from 'react' + import { OtherComponent } from "./other-component"; + interface Props { + text: string; + } + export const MyComponent = (props: Props) => {props.text}; + MyComponent.OtherComponent = OtherComponent; + `, + }, + { + input: ` + import React from 'react' + import { OtherComponent } from "./other-component"; + + type Props = { id: number }; + export const MyComponent: React.FC & { + OtherComponent: typeof OtherComponent; + } = ({ text }) => {text}; + MyComponent.OtherComponent = OtherComponent; + `, + output: ` + import React from 'react' + import { OtherComponent } from "./other-component"; + + type Props = { id: number }; + export const MyComponent = ( { text }: Props ) => {text}; + MyComponent.OtherComponent = OtherComponent; + `, + }, + { + input: ` + import React from 'react' + import { OtherComponent } from "./other-component"; + + type Props = { id: number }; + export const MyComponent: React.FunctionComponent & { + OtherComponent: typeof OtherComponent; + } = (props) => {props.text}; + MyComponent.OtherComponent = OtherComponent; + `, + output: ` + import React from 'react' + import { OtherComponent } from "./other-component"; + + type Props = { id: number }; + export const MyComponent = (props: Props) => {props.text}; + MyComponent.OtherComponent = OtherComponent; + `, + }, + { + input: ` + import React from 'react' + import { OtherComponent } from "./other-component"; + + type Props = { id: number }; + export const MyComponent: React.SFC & { + OtherComponent: typeof OtherComponent; + } = ({ text }) => {text}; + MyComponent.OtherComponent = OtherComponent; + `, + output: ` + import React from 'react' + import { OtherComponent } from "./other-component"; + + type Props = { id: number }; + export const MyComponent = ( { text }: Props ) => {text}; + 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 & { + OtherComponent: typeof OtherComponent; + } = observer((props) => { + return {props.id} + }) + 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 {props.id} + }) + MyComponent.OtherComponent = OtherComponent; + `, + }, ] function escapeLineEndingsAndMultiWhiteSpaces(text: string | null | undefined) { diff --git a/transform.ts b/transform.ts index f38b1d0..c6f825e 100644 --- a/transform.ts +++ b/transform.ts @@ -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) { // 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 } @@ -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 React.SFC, React.FC<{ type: string }>, FC, SFC, and so on