Skip to content

Commit e92f4ad

Browse files
committed
new: Support forwardRef() and memo() components.
1 parent 04db0de commit e92f4ad

File tree

6 files changed

+195
-89
lines changed

6 files changed

+195
-89
lines changed

src/addToFunctionOrVar.ts

Lines changed: 14 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,24 @@ import { types as t } from '@babel/core';
22
import convertToPropTypes from './convertBabelToPropTypes';
33
import extractGenericTypeNames from './extractGenericTypeNames';
44
import { createPropTypesObject, mergePropTypes } from './propTypes';
5-
import { Path, ConvertState } from './types';
6-
7-
function extractTypeNames(path: Path<t.FunctionDeclaration | t.VariableDeclaration>): string[] {
8-
if (t.isFunctionDeclaration(path.node)) {
9-
return extractGenericTypeNames((path.node.params[0] as any).typeAnnotation.typeAnnotation);
10-
}
11-
12-
if (t.isVariableDeclaration(path.node)) {
13-
const decl = path.node.declarations[0];
14-
const id = decl.id as t.Identifier;
15-
16-
if (id.typeAnnotation && id.typeAnnotation.typeAnnotation) {
17-
return extractGenericTypeNames(
18-
(id.typeAnnotation.typeAnnotation as any).typeParameters.params[0],
19-
);
20-
} else if (decl.init && t.isArrowFunctionExpression(decl.init)) {
21-
return extractGenericTypeNames((decl.init.params[0] as any).typeAnnotation.typeAnnotation);
22-
}
23-
}
24-
25-
return [];
26-
}
5+
import { Path, ConvertState, PropTypeDeclaration } from './types';
276

287
function findStaticProperty(
298
path: Path<t.Node>,
309
funcName: string,
3110
name: string,
3211
): t.AssignmentExpression | undefined {
33-
const expr = path
34-
.getAllNextSiblings()
35-
.find(
36-
sibPath =>
37-
t.isExpressionStatement(sibPath.node) &&
38-
t.isAssignmentExpression(sibPath.node.expression, { operator: '=' }) &&
39-
t.isMemberExpression(sibPath.node.expression.left) &&
40-
t.isObjectExpression(sibPath.node.expression.right) &&
41-
t.isIdentifier(sibPath.node.expression.left.object, { name: funcName }) &&
42-
t.isIdentifier(sibPath.node.expression.left.property, { name }),
43-
);
12+
const expr = path.getAllNextSiblings().find(
13+
sibPath =>
14+
t.isExpressionStatement(sibPath.node) &&
15+
t.isAssignmentExpression(sibPath.node.expression, { operator: '=' }) &&
16+
t.isMemberExpression(sibPath.node.expression.left) &&
17+
t.isObjectExpression(sibPath.node.expression.right) &&
18+
t.isIdentifier(sibPath.node.expression.left.object, {
19+
name: funcName,
20+
}) &&
21+
t.isIdentifier(sibPath.node.expression.left.property, { name }),
22+
);
4423

4524
// @ts-ignore
4625
return expr && expr.node.expression;
@@ -49,6 +28,7 @@ function findStaticProperty(
4928
export default function addToFunctionOrVar(
5029
path: Path<t.FunctionDeclaration | t.VariableDeclaration>,
5130
name: string,
31+
propsType: PropTypeDeclaration,
5232
state: ConvertState,
5333
) {
5434
const rootPath =
@@ -70,7 +50,7 @@ export default function addToFunctionOrVar(
7050
});
7151
}
7252

73-
const typeNames = extractTypeNames(path);
53+
const typeNames = extractGenericTypeNames(propsType);
7454
const propTypesList = convertToPropTypes(
7555
state.componentTypes,
7656
typeNames,

src/index.ts

Lines changed: 115 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33
* @license https://opensource.org/licenses/MIT
44
*/
55

6+
/* eslint-disable prefer-destructuring */
7+
68
import { declare } from '@babel/helper-plugin-utils';
79
import { addDefault, addNamed } from '@babel/helper-module-imports';
810
import syntaxTypeScript from '@babel/plugin-syntax-typescript';
911
import { types as t } from '@babel/core';
12+
import { TSTypeParameterInstantiation } from '@babel/types';
1013
import addToClass from './addToClass';
1114
import addToFunctionOrVar from './addToFunctionOrVar';
1215
import extractTypeProperties from './extractTypeProperties';
1316
// import { loadProgram } from './typeChecker';
1417
import upsertImport from './upsertImport';
15-
import { Path, PluginOptions, ConvertState } from './types';
18+
import { Path, PluginOptions, ConvertState, PropTypeDeclaration } from './types';
1619

1720
const BABEL_VERSION = 7;
1821
const MAX_DEPTH = 3;
@@ -27,7 +30,7 @@ function isComponentName(name: string) {
2730
return !!name.match(/^[A-Z]/u);
2831
}
2932

30-
function isPropsParam(param: t.Node) {
33+
function isPropsParam(param: t.Node): param is t.Identifier | t.ObjectPattern {
3134
return (
3235
// (props: Props)
3336
(t.isIdentifier(param) && !!param.typeAnnotation) ||
@@ -36,6 +39,10 @@ function isPropsParam(param: t.Node) {
3639
);
3740
}
3841

42+
function isPropsType(param: t.Node): param is PropTypeDeclaration {
43+
return t.isTSTypeReference(param) || t.isTSIntersectionType(param) || t.isTSUnionType(param);
44+
}
45+
3946
export default declare((api: any, options: PluginOptions, root: string) => {
4047
api.assertVersion(BABEL_VERSION);
4148

@@ -116,7 +123,9 @@ export default declare((api: any, options: PluginOptions, root: string) => {
116123
}
117124

118125
if (node.source.value === 'airbnb-prop-types') {
119-
const response = upsertImport(node, { checkForNamed: 'forbidExtraProps' });
126+
const response = upsertImport(node, {
127+
checkForNamed: 'forbidExtraProps',
128+
});
120129

121130
state.airbnbPropTypes.hasImport = true;
122131
state.airbnbPropTypes.namedImports = response.namedImports;
@@ -164,8 +173,6 @@ export default declare((api: any, options: PluginOptions, root: string) => {
164173

165174
programPath.traverse({
166175
// airbnbPropTypes.componentWithName()
167-
// React.forwardRef()
168-
// React.memo()
169176
CallExpression(path: Path<t.CallExpression>) {
170177
const { node } = path;
171178
const { namedImports } = state.airbnbPropTypes;
@@ -177,28 +184,6 @@ export default declare((api: any, options: PluginOptions, root: string) => {
177184
) {
178185
state.airbnbPropTypes.count += 1;
179186
}
180-
181-
// INCOMPLETE
182-
if (
183-
t.isMemberExpression(node.callee) &&
184-
t.isIdentifier(node.callee.object) &&
185-
t.isIdentifier(node.callee.property) &&
186-
node.callee.object.name === state.reactImportedName &&
187-
(node.callee.property.name === 'forwardRef' || node.callee.property.name === 'memo')
188-
) {
189-
if (
190-
t.isVariableDeclarator(path.parent) &&
191-
t.isVariableDeclaration(path.parentPath.parent)
192-
) {
193-
transformers.push(() =>
194-
addToFunctionOrVar(
195-
path.parentPath.parentPath as any,
196-
((path.parent as t.VariableDeclarator).id as t.Identifier).name,
197-
state,
198-
),
199-
);
200-
}
201-
}
202187
},
203188

204189
// `class Foo extends React.Component<Props> {}`
@@ -232,13 +217,22 @@ export default declare((api: any, options: PluginOptions, root: string) => {
232217
// `function Foo(props: Props) {}`
233218
FunctionDeclaration(path: Path<t.FunctionDeclaration>) {
234219
const { node } = path;
235-
const valid =
220+
221+
if (
236222
!!state.reactImportedName &&
237223
isComponentName(node.id.name) &&
238-
isPropsParam(node.params[0]);
239-
240-
if (valid) {
241-
transformers.push(() => addToFunctionOrVar(path, node.id.name, state));
224+
isPropsParam(node.params[0]) &&
225+
t.isTSTypeAnnotation(node.params[0].typeAnnotation) &&
226+
isPropsType(node.params[0].typeAnnotation.typeAnnotation)
227+
) {
228+
transformers.push(() =>
229+
addToFunctionOrVar(
230+
path,
231+
node.id.name,
232+
(node.params[0] as any).typeAnnotation.typeAnnotation,
233+
state,
234+
),
235+
);
242236
}
243237
},
244238

@@ -253,7 +247,11 @@ export default declare((api: any, options: PluginOptions, root: string) => {
253247

254248
// PropTypes.*
255249
MemberExpression({ node }: Path<t.MemberExpression>) {
256-
if (t.isIdentifier(node.object, { name: state.propTypes.defaultImport })) {
250+
if (
251+
t.isIdentifier(node.object, {
252+
name: state.propTypes.defaultImport,
253+
})
254+
) {
257255
state.propTypes.count += 1;
258256
}
259257
},
@@ -291,6 +289,8 @@ export default declare((api: any, options: PluginOptions, root: string) => {
291289

292290
// `const Foo = (props: Props) => {};`
293291
// `const Foo: React.FC<Props> = () => {};`
292+
// `const Ref = React.forwardRef<Element, Props>();`
293+
// `const Memo = React.memo<Props>();`
294294
VariableDeclaration(path: Path<t.VariableDeclaration>) {
295295
const { node } = path;
296296

@@ -300,39 +300,102 @@ export default declare((api: any, options: PluginOptions, root: string) => {
300300

301301
const decl = node.declarations[0];
302302
const id = decl.id as t.Identifier;
303-
let valid = false;
303+
let props: PropTypeDeclaration | null = null;
304304

305305
// const Foo: React.FC<Props> = () => {};
306306
if (id.typeAnnotation && id.typeAnnotation.typeAnnotation) {
307307
const type = id.typeAnnotation.typeAnnotation;
308308

309-
// prettier-ignore
310-
valid = t.isTSTypeReference(type) &&
309+
if (
310+
t.isTSTypeReference(type) &&
311311
!!type.typeParameters &&
312-
type.typeParameters.params.length > 0 && (
312+
type.typeParameters.params.length > 0 &&
313+
isPropsType(type.typeParameters.params[0]) &&
313314
// React.FC, React.FunctionComponent
314-
(
315-
t.isTSQualifiedName(type.typeName) &&
316-
t.isIdentifier(type.typeName.left, { name: state.reactImportedName }) &&
317-
REACT_FC_NAMES.some(name => t.isIdentifier((type.typeName as any).right, { name }))
318-
) ||
319-
// FC, FunctionComponent
320-
(
321-
!!state.reactImportedName &&
322-
REACT_FC_NAMES.some(name => t.isIdentifier(type.typeName, { name }))
323-
)
324-
);
315+
((t.isTSQualifiedName(type.typeName) &&
316+
t.isIdentifier(type.typeName.left, {
317+
name: state.reactImportedName,
318+
}) &&
319+
REACT_FC_NAMES.some(name =>
320+
t.isIdentifier((type.typeName as any).right, { name }),
321+
)) ||
322+
// FC, FunctionComponent
323+
(!!state.reactImportedName &&
324+
REACT_FC_NAMES.some(name => t.isIdentifier(type.typeName, { name }))))
325+
) {
326+
props = type.typeParameters.params[0];
327+
}
325328

326329
// const Foo = (props: Props) => {};
327330
} else if (t.isArrowFunctionExpression(decl.init)) {
328-
valid =
331+
if (
329332
!!state.reactImportedName &&
330333
isComponentName(id.name) &&
331-
isPropsParam(decl.init.params[0]);
334+
isPropsParam(decl.init.params[0]) &&
335+
t.isTSTypeAnnotation(decl.init.params[0].typeAnnotation) &&
336+
isPropsType(decl.init.params[0].typeAnnotation.typeAnnotation)
337+
) {
338+
props = decl.init.params[0].typeAnnotation.typeAnnotation;
339+
}
340+
341+
// const Ref = React.forwardRef();
342+
// const Memo = React.memo<Props>();
343+
} else if (t.isCallExpression(decl.init)) {
344+
const { init } = decl;
345+
const typeParameters = (init as any).typeParameters as TSTypeParameterInstantiation;
346+
347+
if (
348+
t.isMemberExpression(init.callee) &&
349+
t.isIdentifier(init.callee.object) &&
350+
t.isIdentifier(init.callee.property) &&
351+
init.callee.object.name === state.reactImportedName
352+
) {
353+
if (init.callee.property.name === 'forwardRef') {
354+
// const Ref = React.forwardRef<Element, Props>();
355+
if (
356+
!!typeParameters &&
357+
t.isTSTypeParameterInstantiation(typeParameters) &&
358+
typeParameters.params.length > 1 &&
359+
isPropsType(typeParameters.params[1])
360+
) {
361+
props = typeParameters.params[1];
362+
363+
// const Ref = React.forwardRef((props: Props) => {});
364+
} else if (
365+
t.isArrowFunctionExpression(init.arguments[0]) &&
366+
init.arguments[0].params.length > 0 &&
367+
isPropsParam(init.arguments[0].params[0]) &&
368+
t.isTSTypeAnnotation(init.arguments[0].params[0].typeAnnotation) &&
369+
isPropsType(init.arguments[0].params[0].typeAnnotation.typeAnnotation)
370+
) {
371+
props = init.arguments[0].params[0].typeAnnotation.typeAnnotation;
372+
}
373+
} else if (init.callee.property.name === 'memo') {
374+
// const Ref = React.memo<Props>();
375+
if (
376+
!!typeParameters &&
377+
t.isTSTypeParameterInstantiation(typeParameters) &&
378+
typeParameters.params.length > 0 &&
379+
isPropsType(typeParameters.params[0])
380+
) {
381+
props = typeParameters.params[0];
382+
383+
// const Ref = React.memo((props: Props) => {});
384+
} else if (
385+
t.isArrowFunctionExpression(init.arguments[0]) &&
386+
init.arguments[0].params.length > 0 &&
387+
isPropsParam(init.arguments[0].params[0]) &&
388+
t.isTSTypeAnnotation(init.arguments[0].params[0].typeAnnotation) &&
389+
isPropsType(init.arguments[0].params[0].typeAnnotation.typeAnnotation)
390+
) {
391+
props = init.arguments[0].params[0].typeAnnotation.typeAnnotation;
392+
}
393+
}
394+
}
332395
}
333396

334-
if (valid) {
335-
transformers.push(() => addToFunctionOrVar(path, id.name, state));
397+
if (props) {
398+
transformers.push(() => addToFunctionOrVar(path, id.name, props!, state));
336399
}
337400
},
338401
});

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export interface TypePropertyMap {
77
[key: string]: t.TSPropertySignature[];
88
}
99

10+
export type PropTypeDeclaration = t.TSTypeReference | t.TSIntersectionType | t.TSUnionType;
11+
1012
export type PropType = t.MemberExpression | t.CallExpression | t.Identifier | t.Literal;
1113

1214
export interface PluginOptions {

tests/__snapshots__/index.test.ts.snap

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,53 @@ export function withVar() {
982982
}"
983983
`;
984984
985+
exports[`babel-plugin-typescript-to-proptypes transforms ./fixtures/memo-ref-components.tsx 1`] = `
986+
"import _pt from 'prop-types';
987+
import React from 'react';
988+
interface RefProps {
989+
foo?: string;
990+
ref: React.Ref<HTMLButtonElement>;
991+
}
992+
993+
function BaseRefComp(props: RefProps) {
994+
return null;
995+
}
996+
997+
BaseRefComp.propTypes = {
998+
foo: _pt.string,
999+
ref: _pt.oneOfType([_pt.string, _pt.func, _pt.object]).isRequired
1000+
};
1001+
const RefComp = React.forwardRef<HTMLButtonElement, RefProps>((props, ref) => <BaseRefComp ref={ref} {...props} />);
1002+
RefComp.propTypes = {
1003+
foo: _pt.string,
1004+
ref: _pt.oneOfType([_pt.string, _pt.func, _pt.object]).isRequired
1005+
};
1006+
const RefCompAlt = React.forwardRef((props: RefProps, ref: React.Ref<HTMLButtonElement>) => <BaseRefComp ref={ref} {...props} />);
1007+
RefCompAlt.propTypes = {
1008+
foo: _pt.string,
1009+
ref: _pt.oneOfType([_pt.string, _pt.func, _pt.object]).isRequired
1010+
};
1011+
const RefCompNoTypes = React.forwardRef((props, ref) => null);
1012+
interface MemoProps {
1013+
bar: number;
1014+
}
1015+
const MemoComp = React.memo<MemoProps>(props => {
1016+
return null;
1017+
});
1018+
MemoComp.propTypes = {
1019+
bar: _pt.number.isRequired
1020+
};
1021+
const MemoCompAlt = React.memo((props: MemoProps) => {
1022+
return null;
1023+
});
1024+
MemoCompAlt.propTypes = {
1025+
bar: _pt.number.isRequired
1026+
};
1027+
const MemoCompNoTypes = React.memo(props => {
1028+
return null;
1029+
});"
1030+
`;
1031+
9851032
exports[`babel-plugin-typescript-to-proptypes transforms ./fixtures/multiple-components.ts 1`] = `
9861033
"import _pt from 'prop-types';
9871034
import React from 'react';

0 commit comments

Comments
 (0)