Skip to content

Commit be78a60

Browse files
rtsaofkling
authored andcommitted
Added support for resolving exported components within HOCs (#124)
1 parent d65bfc4 commit be78a60

File tree

6 files changed

+266
-5
lines changed

6 files changed

+266
-5
lines changed

src/resolver/__tests__/findAllExportedComponentDefinitions-test.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,23 @@ describe('findAllExportedComponentDefinitions', () => {
181181
expect(actual[1].node).toBe(expectedB.node);
182182
});
183183

184+
it('finds multiple exported components with hocs', () => {
185+
var parsed = parse(`
186+
var R = require("React");
187+
var ComponentA = R.createClass({});
188+
var ComponentB = R.createClass({});
189+
exports.ComponentA = hoc(ComponentA);
190+
exports.ComponentB = hoc(ComponentB);
191+
`);
192+
var actual = findComponents(parsed);
193+
var expectedA = parsed.get('body', 1, 'declarations', 0, 'init', 'arguments', 0);
194+
var expectedB = parsed.get('body', 2, 'declarations', 0, 'init', 'arguments', 0);
195+
196+
expect(actual.length).toBe(2);
197+
expect(actual[0].node).toBe(expectedA.node);
198+
expect(actual[1].node).toBe(expectedB.node);
199+
});
200+
184201
it('finds only exported components', () => {
185202
var parsed = parse(`
186203
var R = require("React");
@@ -700,6 +717,18 @@ describe('findAllExportedComponentDefinitions', () => {
700717
expect(actual.length).toBe(2);
701718
});
702719

720+
it('finds multiple components with hocs', () => {
721+
var parsed = parse(`
722+
var R = require("React");
723+
var ComponentA = hoc(R.createClass({}));
724+
var ComponentB = hoc(R.createClass({}));
725+
export {ComponentA as foo, ComponentB};
726+
`);
727+
var actual = findComponents(parsed);
728+
729+
expect(actual.length).toBe(2);
730+
});
731+
703732
it('finds only exported components', () => {
704733
var parsed = parse(`
705734
var R = require("React");
@@ -774,6 +803,20 @@ describe('findAllExportedComponentDefinitions', () => {
774803
expect(actual.length).toBe(2);
775804
});
776805

806+
it('finds multiple components with hocs', () => {
807+
var parsed = parse(`
808+
import React from 'React';
809+
class ComponentA extends React.Component {};
810+
class ComponentB extends React.Component {};
811+
var WrappedA = hoc(ComponentA);
812+
var WrappedB = hoc(ComponentB);
813+
export {WrappedA, WrappedB};
814+
`);
815+
var actual = findComponents(parsed);
816+
817+
expect(actual.length).toBe(2);
818+
});
819+
777820
it('finds only exported components', () => {
778821
var parsed = parse(`
779822
import React from 'React';

src/resolver/__tests__/findExportedComponentDefinition-test.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,52 @@ describe('findExportedComponentDefinition', () => {
4545
expect(parse(source)).toBeDefined();
4646
});
4747

48+
it('finds React.createClass with hoc', () => {
49+
var source = `
50+
var React = require("React");
51+
var Component = React.createClass({});
52+
module.exports = hoc(Component);
53+
`;
54+
55+
expect(parse(source)).toBeDefined();
56+
});
57+
58+
it('finds React.createClass with hoc and args', () => {
59+
var source = `
60+
var React = require("React");
61+
var Component = React.createClass({});
62+
module.exports = hoc(arg1, arg2)(Component);
63+
`;
64+
65+
expect(parse(source)).toBeDefined();
66+
});
67+
68+
it('finds React.createClass with two hocs', () => {
69+
var source = `
70+
var React = require("React");
71+
var Component = React.createClass({});
72+
module.exports = hoc2(arg2b, arg2b)(
73+
hoc1(arg1a, arg2a)(Component)
74+
);
75+
`;
76+
77+
expect(parse(source)).toBeDefined();
78+
});
79+
80+
it('finds React.createClass with three hocs', () => {
81+
var source = `
82+
var React = require("React");
83+
var Component = React.createClass({});
84+
module.exports = hoc3(arg3a, arg3b)(
85+
hoc2(arg2b, arg2b)(
86+
hoc1(arg1a, arg2a)(Component)
87+
)
88+
);
89+
`;
90+
91+
expect(parse(source)).toBeDefined();
92+
});
93+
4894
it('finds React.createClass, independent of the var name', () => {
4995
var source = `
5096
var R = require("React");
@@ -317,6 +363,46 @@ describe('findExportedComponentDefinition', () => {
317363
expect(result.node.type).toBe('ClassDeclaration');
318364
});
319365

366+
367+
it('finds default export with hoc', () => {
368+
var source = `
369+
import React from 'React';
370+
class Component extends React.Component {}
371+
export default hoc(Component);
372+
`;
373+
374+
var result = parse(source);
375+
expect(result).toBeDefined();
376+
expect(result.node.type).toBe('ClassDeclaration');
377+
378+
});
379+
380+
it('finds default export with hoc and args', () => {
381+
var source = `
382+
import React from 'React';
383+
class Component extends React.Component {}
384+
export default hoc(arg1, arg2)(Component);
385+
`;
386+
387+
var result = parse(source);
388+
expect(result).toBeDefined();
389+
expect(result.node.type).toBe('ClassDeclaration');
390+
});
391+
392+
it('finds default export with two hocs', () => {
393+
var source = `
394+
import React from 'React';
395+
class Component extends React.Component {}
396+
export default hoc2(arg2b, arg2b)(
397+
hoc1(arg1a, arg2a)(Component)
398+
);
399+
`;
400+
401+
var result = parse(source);
402+
expect(result).toBeDefined();
403+
expect(result.node.type).toBe('ClassDeclaration');
404+
});
405+
320406
it('errors if multiple components are exported', () => {
321407
var source = `
322408
import React from 'React';

src/resolver/findAllExportedComponentDefinitions.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import isStatelessComponent from '../utils/isStatelessComponent';
1616
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
1717
import resolveExportDeclaration from '../utils/resolveExportDeclaration';
1818
import resolveToValue from '../utils/resolveToValue';
19+
import resolveHOC from '../utils/resolveHOC';
1920

2021
function ignore() {
2122
return false;
@@ -65,7 +66,17 @@ export default function findExportedComponentDefinitions(
6566

6667
function exportDeclaration(path) {
6768
var definitions: Array<?NodePath> = resolveExportDeclaration(path, types)
68-
.filter(isComponentDefinition)
69+
.reduce((acc, definition) => {
70+
if (isComponentDefinition(definition)) {
71+
acc.push(definition);
72+
} else {
73+
var resolved = resolveToValue(resolveHOC(definition));
74+
if (isComponentDefinition(resolved)) {
75+
acc.push(resolved);
76+
}
77+
}
78+
return acc;
79+
}, [])
6980
.map((definition) => resolveDefinition(definition, types));
7081

7182
if (definitions.length === 0) {
@@ -107,7 +118,10 @@ export default function findExportedComponentDefinitions(
107118
// expression, something like React.createClass
108119
path = resolveToValue(path.get('right'));
109120
if (!isComponentDefinition(path)) {
110-
return false;
121+
path = resolveToValue(resolveHOC(path));
122+
if (!isComponentDefinition(path)) {
123+
return false;
124+
}
111125
}
112126
const definition = resolveDefinition(path, types);
113127
if (definition && components.indexOf(definition) === -1) {

src/resolver/findExportedComponentDefinition.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import isStatelessComponent from '../utils/isStatelessComponent';
1616
import normalizeClassDefinition from '../utils/normalizeClassDefinition';
1717
import resolveExportDeclaration from '../utils/resolveExportDeclaration';
1818
import resolveToValue from '../utils/resolveToValue';
19+
import resolveHOC from '../utils/resolveHOC';
1920

2021
var ERROR_MULTIPLE_DEFINITIONS =
2122
'Multiple exported component definitions found.';
@@ -35,7 +36,7 @@ function resolveDefinition(definition, types) {
3536
if (types.ObjectExpression.check(resolvedPath.node)) {
3637
return resolvedPath;
3738
}
38-
} else if(isReactComponentClass(definition)) {
39+
} else if (isReactComponentClass(definition)) {
3940
normalizeClassDefinition(definition);
4041
return definition;
4142
} else if (isStatelessComponent(definition)) {
@@ -68,7 +69,17 @@ export default function findExportedComponentDefinition(
6869

6970
function exportDeclaration(path) {
7071
var definitions = resolveExportDeclaration(path, types)
71-
.filter(isComponentDefinition);
72+
.reduce((acc, definition) => {
73+
if (isComponentDefinition(definition)) {
74+
acc.push(definition);
75+
} else {
76+
var resolved = resolveToValue(resolveHOC(definition));
77+
if (isComponentDefinition(resolved)) {
78+
acc.push(resolved);
79+
}
80+
}
81+
return acc;
82+
}, []);
7283

7384
if (definitions.length === 0) {
7485
return false;
@@ -109,7 +120,10 @@ export default function findExportedComponentDefinition(
109120
// expression, something like React.createClass
110121
path = resolveToValue(path.get('right'));
111122
if (!isComponentDefinition(path)) {
112-
return false;
123+
path = resolveToValue(resolveHOC(path));
124+
if (!isComponentDefinition(path)) {
125+
return false;
126+
}
113127
}
114128
if (definition) {
115129
// If a file exports multiple components, ... complain!
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
*/
10+
11+
/*global jest, describe, beforeEach, it, expect*/
12+
13+
jest.disableAutomock();
14+
15+
describe('resolveHOC', () => {
16+
var builders;
17+
var utils;
18+
var resolveHOC;
19+
20+
function parse(src) {
21+
var root = utils.parse(src);
22+
return root.get('body', root.node.body.length - 1, 'expression');
23+
}
24+
25+
beforeEach(() => {
26+
var recast = require('recast');
27+
builders = recast.types.builders;
28+
resolveHOC = require('../resolveHOC').default;
29+
utils = require('../../../tests/utils');
30+
});
31+
32+
it('resolves simple hoc', () => {
33+
var path = parse([
34+
'hoc(42);',
35+
].join('\n'));
36+
expect(resolveHOC(path).node).toEqualASTNode(builders.literal(42));
37+
});
38+
39+
it('resolves simple hoc w/ multiple args', () => {
40+
var path = parse([
41+
'hoc1(arg1a, arg1b)(42);',
42+
].join('\n'));
43+
expect(resolveHOC(path).node).toEqualASTNode(builders.literal(42));
44+
});
45+
46+
it('resolves nested hocs', () => {
47+
var path = parse([
48+
'hoc2(arg2b, arg2b)(',
49+
' hoc1(arg1a, arg2a)(42)',
50+
');',
51+
].join('\n'));
52+
expect(resolveHOC(path).node).toEqualASTNode(builders.literal(42));
53+
});
54+
55+
it('resolves really nested hocs', () => {
56+
var path = parse([
57+
'hoc3(arg3a, arg3b)(',
58+
' hoc2(arg2b, arg2b)(',
59+
' hoc1(arg1a, arg2a)(42)',
60+
' )',
61+
');',
62+
].join('\n'));
63+
expect(resolveHOC(path).node).toEqualASTNode(builders.literal(42));
64+
});
65+
66+
});

src/utils/resolveHOC.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) 2015, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @flow
10+
*
11+
*/
12+
13+
import recast from 'recast';
14+
import isReactCreateClassCall from './isReactCreateClassCall';
15+
16+
var {
17+
types: {
18+
NodePath,
19+
namedTypes: types,
20+
},
21+
} = recast;
22+
23+
/**
24+
* If the path is a call expression, it recursively resolves to the
25+
* rightmost argument, stopping if it finds a React.createClass call expression
26+
*
27+
* Else the path itself is returned.
28+
*/
29+
export default function resolveHOC(path: NodePath): NodePath {
30+
var node = path.node;
31+
if (types.CallExpression.check(node) && !isReactCreateClassCall(path)) {
32+
if (node.arguments.length) {
33+
return resolveHOC(path.get('arguments', node.arguments.length - 1));
34+
}
35+
}
36+
37+
return path;
38+
}

0 commit comments

Comments
 (0)