Skip to content

Commit

Permalink
feat: support forwardRef for all resolvers (#324)
Browse files Browse the repository at this point in the history
  • Loading branch information
jquense authored and danez committed Feb 7, 2019
1 parent 55695c3 commit 5c01cd7
Show file tree
Hide file tree
Showing 6 changed files with 204 additions and 17 deletions.
40 changes: 40 additions & 0 deletions src/resolver/__tests__/findAllComponentDefinitions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,44 @@ describe('findAllComponentDefinitions', () => {
expect(result.length).toBe(0);
});
});

describe('forwardRef components', () => {
it('finds forwardRef components', () => {
const source = `
import React from 'react';
import PropTypes from 'prop-types';
import extendStyles from 'enhancers/extendStyles';
const ColoredView = React.forwardRef((props, ref) => (
<div ref={ref} style={{backgroundColor: props.color}} />
));
extendStyles(ColoredView);
`;

const result = parse(source);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(1);
expect(result[0].value.type).toEqual('CallExpression');
});

it('finds none inline forwardRef components', () => {
const source = `
import React from 'react';
import PropTypes from 'prop-types';
import extendStyles from 'enhancers/extendStyles';
function ColoredView(props, ref) {
return <div ref={ref} style={{backgroundColor: props.color}} />
}
const ForwardedColoredView = React.forwardRef(ColoredView);
`;

const result = parse(source);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(1);
expect(result[0].value.type).toEqual('CallExpression');
});
});
});
130 changes: 130 additions & 0 deletions src/resolver/__tests__/findAllExportedComponentDefinitions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,50 @@ describe('findAllExportedComponentDefinitions', () => {
});
});

describe('forwardRef components', () => {
it('finds forwardRef components', () => {
const source = `
import React from 'react';
import PropTypes from 'prop-types';
import extendStyles from 'enhancers/extendStyles';
const ColoredView = React.forwardRef((props, ref) => (
<div ref={ref} style={{backgroundColor: props.color}} />
));
module.exports = extendStyles(ColoredView);
`;

const parsed = parse(source);
const actual = findComponents(parsed);

expect(actual.length).toBe(1);
expect(actual[0].value.type).toEqual('CallExpression');
});

it('finds none inline forwardRef components', () => {
const source = `
import React from 'react';
import PropTypes from 'prop-types';
import extendStyles from 'enhancers/extendStyles';
function ColoredView(props, ref) {
return <div ref={ref} style={{backgroundColor: props.color}} />
}
const ForwardedColoredView = React.forwardRef(ColoredView);
module.exports = ForwardedColoredView
`;

const parsed = parse(source);
const actual = findComponents(parsed);

expect(actual.length).toBe(1);
expect(actual[0].value.type).toEqual('CallExpression');
});
});

describe('module.exports = <C>; / exports.foo = <C>;', () => {
describe('React.createClass', () => {
it('finds assignments to exports', () => {
Expand Down Expand Up @@ -486,6 +530,50 @@ describe('findAllExportedComponentDefinitions', () => {
expect(actual.length).toBe(1);
});
});

describe('forwardRef components', () => {
it('finds forwardRef components', () => {
const source = `
import React from 'react';
import PropTypes from 'prop-types';
import extendStyles from 'enhancers/extendStyles';
const ColoredView = React.forwardRef((props, ref) => (
<div ref={ref} style={{backgroundColor: props.color}} />
));
export default extendStyles(ColoredView);
`;

const parsed = parse(source);
const actual = findComponents(parsed);

expect(actual.length).toBe(1);
expect(actual[0].value.type).toEqual('CallExpression');
});

it('finds none inline forwardRef components', () => {
const source = `
import React from 'react';
import PropTypes from 'prop-types';
import extendStyles from 'enhancers/extendStyles';
function ColoredView(props, ref) {
return <div ref={ref} style={{backgroundColor: props.color}} />
}
const ForwardedColoredView = React.forwardRef(ColoredView);
export default ForwardedColoredView
`;

const parsed = parse(source);
const actual = findComponents(parsed);

expect(actual.length).toBe(1);
expect(actual[0].value.type).toEqual('CallExpression');
});
});
});

describe('export var foo = <C>, ...;', () => {
Expand Down Expand Up @@ -734,6 +822,26 @@ describe('findAllExportedComponentDefinitions', () => {
expect(actual[0].node.type).toBe('FunctionExpression');
});
});

describe('forwardRef components', () => {
it('finds forwardRef components', () => {
const source = `
import React from 'react';
import PropTypes from 'prop-types';
import extendStyles from 'enhancers/extendStyles';
export const ColoredView = extendStyles(React.forwardRef((props, ref) => (
<div ref={ref} style={{backgroundColor: props.color}} />
)));
`;

const parsed = parse(source);
const actual = findComponents(parsed);

expect(actual.length).toBe(1);
expect(actual[0].value.type).toEqual('CallExpression');
});
});
});

describe('export {<C>};', () => {
Expand Down Expand Up @@ -994,6 +1102,28 @@ describe('findAllExportedComponentDefinitions', () => {
expect(actual[0].node.type).toBe('ArrowFunctionExpression');
});
});

describe('forwardRef components', () => {
it('finds forwardRef components', () => {
const source = `
import React from 'react';
import PropTypes from 'prop-types';
import extendStyles from 'enhancers/extendStyles';
const ColoredView = extendStyles(React.forwardRef((props, ref) => (
<div ref={ref} style={{backgroundColor: props.color}} />
)));
export { ColoredView }
`;

const parsed = parse(source);
const actual = findComponents(parsed);

expect(actual.length).toBe(1);
expect(actual[0].value.type).toEqual('CallExpression');
});
});
});

describe('export <C>;', () => {
Expand Down
26 changes: 16 additions & 10 deletions src/resolver/findAllComponentDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import isReactComponentClass from '../utils/isReactComponentClass';
import isReactCreateClassCall from '../utils/isReactCreateClassCall';
import isReactForwardRefCall from '../utils/isReactForwardRefCall';
import isStatelessComponent from '../utils/isStatelessComponent';
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
import resolveToValue from '../utils/resolveToValue';
Expand All @@ -25,19 +26,19 @@ export default function findAllReactCreateClassCalls(
recast: Object,
): Array<NodePath> {
const types = recast.types.namedTypes;
const definitions = [];
const definitions = new Set();

function classVisitor(path) {
if (isReactComponentClass(path)) {
normalizeClassDefinition(path);
definitions.push(path);
definitions.add(path);
}
return false;
}

function statelessVisitor(path) {
if (isStatelessComponent(path)) {
definitions.push(path);
definitions.add(path);
}
return false;
}
Expand All @@ -49,16 +50,21 @@ export default function findAllReactCreateClassCalls(
visitClassExpression: classVisitor,
visitClassDeclaration: classVisitor,
visitCallExpression: function(path) {
if (!isReactCreateClassCall(path)) {
return false;
}
const resolvedPath = resolveToValue(path.get('arguments', 0));
if (types.ObjectExpression.check(resolvedPath.node)) {
definitions.push(resolvedPath);
if (isReactForwardRefCall(path)) {
// If the the inner function was previously identified as a component
// replace it with the parent node
const inner = resolveToValue(path.get('arguments', 0));
definitions.delete(inner);
definitions.add(path);
} else if (isReactCreateClassCall(path)) {
const resolvedPath = resolveToValue(path.get('arguments', 0));
if (types.ObjectExpression.check(resolvedPath.node)) {
definitions.add(resolvedPath);
}
}
return false;
},
});

return definitions;
return Array.from(definitions);
}
9 changes: 7 additions & 2 deletions src/resolver/findAllExportedComponentDefinitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import isExportsOrModuleAssignment from '../utils/isExportsOrModuleAssignment';
import isReactComponentClass from '../utils/isReactComponentClass';
import isReactCreateClassCall from '../utils/isReactCreateClassCall';
import isReactForwardRefCall from '../utils/isReactForwardRefCall';
import isStatelessComponent from '../utils/isStatelessComponent';
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
import resolveExportDeclaration from '../utils/resolveExportDeclaration';
Expand All @@ -26,7 +27,8 @@ function isComponentDefinition(path) {
return (
isReactCreateClassCall(path) ||
isReactComponentClass(path) ||
isStatelessComponent(path)
isStatelessComponent(path) ||
isReactForwardRefCall(path)
);
}

Expand All @@ -40,7 +42,10 @@ function resolveDefinition(definition, types): ?NodePath {
} else if (isReactComponentClass(definition)) {
normalizeClassDefinition(definition);
return definition;
} else if (isStatelessComponent(definition)) {
} else if (
isStatelessComponent(definition) ||
isReactForwardRefCall(definition)
) {
return definition;
}
return null;
Expand Down
9 changes: 5 additions & 4 deletions src/resolver/findExportedComponentDefinition.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
*/

import isExportsOrModuleAssignment from '../utils/isExportsOrModuleAssignment';
import isReactForwardRefCall from '../utils/isReactForwardRefCall';
import isReactComponentClass from '../utils/isReactComponentClass';
import isReactCreateClassCall from '../utils/isReactCreateClassCall';
import isReactForwardRefCall from '../utils/isReactForwardRefCall';
import isStatelessComponent from '../utils/isStatelessComponent';
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
import resolveExportDeclaration from '../utils/resolveExportDeclaration';
Expand Down Expand Up @@ -45,9 +45,10 @@ function resolveDefinition(definition, types) {
} else if (isReactComponentClass(definition)) {
normalizeClassDefinition(definition);
return definition;
} else if (isStatelessComponent(definition)) {
return definition;
} else if (isReactForwardRefCall(definition)) {
} else if (
isStatelessComponent(definition) ||
isReactForwardRefCall(definition)
) {
return definition;
}
return null;
Expand Down
7 changes: 6 additions & 1 deletion src/utils/resolveHOC.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import recast from 'recast';
import isReactCreateClassCall from './isReactCreateClassCall';
import isReactForwardRefCall from './isReactForwardRefCall';

const {
types: { NodePath, namedTypes: types },
Expand All @@ -25,7 +26,11 @@ const {
*/
export default function resolveHOC(path: NodePath): NodePath {
const node = path.node;
if (types.CallExpression.check(node) && !isReactCreateClassCall(path)) {
if (
types.CallExpression.check(node) &&
!isReactCreateClassCall(path) &&
!isReactForwardRefCall(path)
) {
if (node.arguments.length) {
return resolveHOC(path.get('arguments', node.arguments.length - 1));
}
Expand Down

0 comments on commit 5c01cd7

Please sign in to comment.