From 92cedc3052a6a0fa843f84133b381f330e912a95 Mon Sep 17 00:00:00 2001 From: Ryan Tsao Date: Tue, 13 Sep 2016 17:00:22 -0700 Subject: [PATCH] Added support for resolving exported components within HOCs --- ...indAllExportedComponentDefinitions-test.js | 43 ++++++++++ .../findExportedComponentDefinition-test.js | 86 +++++++++++++++++++ .../findAllExportedComponentDefinitions.js | 18 +++- .../findExportedComponentDefinition.js | 20 ++++- src/utils/__tests__/resolveHOC-test.js | 66 ++++++++++++++ src/utils/resolveHOC.js | 38 ++++++++ 6 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 src/utils/__tests__/resolveHOC-test.js create mode 100644 src/utils/resolveHOC.js diff --git a/src/resolver/__tests__/findAllExportedComponentDefinitions-test.js b/src/resolver/__tests__/findAllExportedComponentDefinitions-test.js index cc5039fe49a..20e7c1a57aa 100644 --- a/src/resolver/__tests__/findAllExportedComponentDefinitions-test.js +++ b/src/resolver/__tests__/findAllExportedComponentDefinitions-test.js @@ -181,6 +181,23 @@ describe('findAllExportedComponentDefinitions', () => { expect(actual[1].node).toBe(expectedB.node); }); + it('finds multiple exported components with hocs', () => { + var parsed = parse(` + var R = require("React"); + var ComponentA = R.createClass({}); + var ComponentB = R.createClass({}); + exports.ComponentA = hoc(ComponentA); + exports.ComponentB = hoc(ComponentB); + `); + var actual = findComponents(parsed); + var expectedA = parsed.get('body', 1, 'declarations', 0, 'init', 'arguments', 0); + var expectedB = parsed.get('body', 2, 'declarations', 0, 'init', 'arguments', 0); + + expect(actual.length).toBe(2); + expect(actual[0].node).toBe(expectedA.node); + expect(actual[1].node).toBe(expectedB.node); + }); + it('finds only exported components', () => { var parsed = parse(` var R = require("React"); @@ -700,6 +717,18 @@ describe('findAllExportedComponentDefinitions', () => { expect(actual.length).toBe(2); }); + it('finds multiple components with hocs', () => { + var parsed = parse(` + var R = require("React"); + var ComponentA = hoc(R.createClass({})); + var ComponentB = hoc(R.createClass({})); + export {ComponentA as foo, ComponentB}; + `); + var actual = findComponents(parsed); + + expect(actual.length).toBe(2); + }); + it('finds only exported components', () => { var parsed = parse(` var R = require("React"); @@ -774,6 +803,20 @@ describe('findAllExportedComponentDefinitions', () => { expect(actual.length).toBe(2); }); + it('finds multiple components with hocs', () => { + var parsed = parse(` + import React from 'React'; + class ComponentA extends React.Component {}; + class ComponentB extends React.Component {}; + var WrappedA = hoc(ComponentA); + var WrappedB = hoc(ComponentB); + export {WrappedA, WrappedB}; + `); + var actual = findComponents(parsed); + + expect(actual.length).toBe(2); + }); + it('finds only exported components', () => { var parsed = parse(` import React from 'React'; diff --git a/src/resolver/__tests__/findExportedComponentDefinition-test.js b/src/resolver/__tests__/findExportedComponentDefinition-test.js index d0a64a174c9..c5a1942f9e6 100644 --- a/src/resolver/__tests__/findExportedComponentDefinition-test.js +++ b/src/resolver/__tests__/findExportedComponentDefinition-test.js @@ -45,6 +45,52 @@ describe('findExportedComponentDefinition', () => { expect(parse(source)).toBeDefined(); }); + it('finds React.createClass with hoc', () => { + var source = ` + var React = require("React"); + var Component = React.createClass({}); + module.exports = hoc(Component); + `; + + expect(parse(source)).toBeDefined(); + }); + + it('finds React.createClass with hoc and args', () => { + var source = ` + var React = require("React"); + var Component = React.createClass({}); + module.exports = hoc(arg1, arg2)(Component); + `; + + expect(parse(source)).toBeDefined(); + }); + + it('finds React.createClass with two hocs', () => { + var source = ` + var React = require("React"); + var Component = React.createClass({}); + module.exports = hoc2(arg2b, arg2b)( + hoc1(arg1a, arg2a)(Component) + ); + `; + + expect(parse(source)).toBeDefined(); + }); + + it('finds React.createClass with three hocs', () => { + var source = ` + var React = require("React"); + var Component = React.createClass({}); + module.exports = hoc3(arg3a, arg3b)( + hoc2(arg2b, arg2b)( + hoc1(arg1a, arg2a)(Component) + ) + ); + `; + + expect(parse(source)).toBeDefined(); + }); + it('finds React.createClass, independent of the var name', () => { var source = ` var R = require("React"); @@ -317,6 +363,46 @@ describe('findExportedComponentDefinition', () => { expect(result.node.type).toBe('ClassDeclaration'); }); + + it('finds default export with hoc', () => { + var source = ` + import React from 'React'; + class Component extends React.Component {} + export default hoc(Component); + `; + + var result = parse(source); + expect(result).toBeDefined(); + expect(result.node.type).toBe('ClassDeclaration'); + + }); + + it('finds default export with hoc and args', () => { + var source = ` + import React from 'React'; + class Component extends React.Component {} + export default hoc(arg1, arg2)(Component); + `; + + var result = parse(source); + expect(result).toBeDefined(); + expect(result.node.type).toBe('ClassDeclaration'); + }); + + it('finds default export with two hocs', () => { + var source = ` + import React from 'React'; + class Component extends React.Component {} + export default hoc2(arg2b, arg2b)( + hoc1(arg1a, arg2a)(Component) + ); + `; + + var result = parse(source); + expect(result).toBeDefined(); + expect(result.node.type).toBe('ClassDeclaration'); + }); + it('errors if multiple components are exported', () => { var source = ` import React from 'React'; diff --git a/src/resolver/findAllExportedComponentDefinitions.js b/src/resolver/findAllExportedComponentDefinitions.js index 4fb02d6979a..3f8cc6c99ed 100644 --- a/src/resolver/findAllExportedComponentDefinitions.js +++ b/src/resolver/findAllExportedComponentDefinitions.js @@ -16,6 +16,7 @@ import isStatelessComponent from '../utils/isStatelessComponent'; import normalizeClassDefinition from '../utils/normalizeClassDefinition'; import resolveExportDeclaration from '../utils/resolveExportDeclaration'; import resolveToValue from '../utils/resolveToValue'; +import resolveHOC from '../utils/resolveHOC'; function ignore() { return false; @@ -65,7 +66,17 @@ export default function findExportedComponentDefinitions( function exportDeclaration(path) { var definitions: Array = resolveExportDeclaration(path, types) - .filter(isComponentDefinition) + .reduce((acc, definition) => { + if (isComponentDefinition(definition)) { + acc.push(definition); + } else { + var resolved = resolveToValue(resolveHOC(definition)); + if (isComponentDefinition(resolved)) { + acc.push(resolved); + } + } + return acc; + }, []) .map((definition) => resolveDefinition(definition, types)); if (definitions.length === 0) { @@ -107,7 +118,10 @@ export default function findExportedComponentDefinitions( // expression, something like React.createClass path = resolveToValue(path.get('right')); if (!isComponentDefinition(path)) { - return false; + path = resolveToValue(resolveHOC(path)); + if (!isComponentDefinition(path)) { + return false; + } } const definition = resolveDefinition(path, types); if (definition && components.indexOf(definition) === -1) { diff --git a/src/resolver/findExportedComponentDefinition.js b/src/resolver/findExportedComponentDefinition.js index 785e88450fb..de1ff91cd09 100644 --- a/src/resolver/findExportedComponentDefinition.js +++ b/src/resolver/findExportedComponentDefinition.js @@ -16,6 +16,7 @@ import isStatelessComponent from '../utils/isStatelessComponent'; import normalizeClassDefinition from '../utils/normalizeClassDefinition'; import resolveExportDeclaration from '../utils/resolveExportDeclaration'; import resolveToValue from '../utils/resolveToValue'; +import resolveHOC from '../utils/resolveHOC'; var ERROR_MULTIPLE_DEFINITIONS = 'Multiple exported component definitions found.'; @@ -35,7 +36,7 @@ function resolveDefinition(definition, types) { if (types.ObjectExpression.check(resolvedPath.node)) { return resolvedPath; } - } else if(isReactComponentClass(definition)) { + } else if (isReactComponentClass(definition)) { normalizeClassDefinition(definition); return definition; } else if (isStatelessComponent(definition)) { @@ -68,7 +69,17 @@ export default function findExportedComponentDefinition( function exportDeclaration(path) { var definitions = resolveExportDeclaration(path, types) - .filter(isComponentDefinition); + .reduce((acc, definition) => { + if (isComponentDefinition(definition)) { + acc.push(definition); + } else { + var resolved = resolveToValue(resolveHOC(definition)); + if (isComponentDefinition(resolved)) { + acc.push(resolved); + } + } + return acc; + }, []); if (definitions.length === 0) { return false; @@ -109,7 +120,10 @@ export default function findExportedComponentDefinition( // expression, something like React.createClass path = resolveToValue(path.get('right')); if (!isComponentDefinition(path)) { - return false; + path = resolveToValue(resolveHOC(path)); + if (!isComponentDefinition(path)) { + return false; + } } if (definition) { // If a file exports multiple components, ... complain! diff --git a/src/utils/__tests__/resolveHOC-test.js b/src/utils/__tests__/resolveHOC-test.js new file mode 100644 index 00000000000..dcbf244a9e2 --- /dev/null +++ b/src/utils/__tests__/resolveHOC-test.js @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +/*global jest, describe, beforeEach, it, expect*/ + +jest.disableAutomock(); + +describe('resolveHOC', () => { + var builders; + var utils; + var resolveHOC; + + function parse(src) { + var root = utils.parse(src); + return root.get('body', root.node.body.length - 1, 'expression'); + } + + beforeEach(() => { + var recast = require('recast'); + builders = recast.types.builders; + resolveHOC = require('../resolveHOC').default; + utils = require('../../../tests/utils'); + }); + + it('resolves simple hoc', () => { + var path = parse([ + 'hoc(42);', + ].join('\n')); + expect(resolveHOC(path).node).toEqualASTNode(builders.literal(42)); + }); + + it('resolves simple hoc w/ multiple args', () => { + var path = parse([ + 'hoc1(arg1a, arg1b)(42);', + ].join('\n')); + expect(resolveHOC(path).node).toEqualASTNode(builders.literal(42)); + }); + + it('resolves nested hocs', () => { + var path = parse([ + 'hoc2(arg2b, arg2b)(', + ' hoc1(arg1a, arg2a)(42)', + ');', + ].join('\n')); + expect(resolveHOC(path).node).toEqualASTNode(builders.literal(42)); + }); + + it('resolves really nested hocs', () => { + var path = parse([ + 'hoc3(arg3a, arg3b)(', + ' hoc2(arg2b, arg2b)(', + ' hoc1(arg1a, arg2a)(42)', + ' )', + ');', + ].join('\n')); + expect(resolveHOC(path).node).toEqualASTNode(builders.literal(42)); + }); + +}); diff --git a/src/utils/resolveHOC.js b/src/utils/resolveHOC.js new file mode 100644 index 00000000000..75d479b4521 --- /dev/null +++ b/src/utils/resolveHOC.js @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2015, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @flow + * + */ + +import recast from 'recast'; +import isReactCreateClassCall from './isReactCreateClassCall'; + +var { + types: { + NodePath, + namedTypes: types, + }, +} = recast; + +/** + * If the path is a call expression, it recursively resolves to the + * rightmost argument, stopping if it finds a React.createClass call expression + * + * Else the path itself is returned. + */ +export default function resolveHOC(path: NodePath): NodePath { + var node = path.node; + if (types.CallExpression.check(node) && !isReactCreateClassCall(path)) { + if (node.arguments.length) { + return resolveHOC(path.get('arguments', node.arguments.length - 1)); + } + } + + return path; +}