Skip to content

Commit 77c780b

Browse files
committed
Initial support for importing prop types
1 parent 16cc596 commit 77c780b

16 files changed

+394
-23
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"commander": "^2.19.0",
4545
"doctrine": "^3.0.0",
4646
"node-dir": "^0.1.10",
47-
"strip-indent": "^2.0.0"
47+
"strip-indent": "^2.0.0",
48+
"resolve": "^1.10.1"
4849
},
4950
"devDependencies": {
5051
"@babel/cli": "^7.0.0",

src/__tests__/__snapshots__/main-test.js.snap

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,3 +1421,144 @@ Object {
14211421
},
14221422
}
14231423
`;
1424+
1425+
exports[`main fixtures processes component "component_28.tsx" without errors 1`] = `
1426+
Object {
1427+
"description": "This is a typescript component with imported prop types",
1428+
"displayName": "ImportedComponent",
1429+
"methods": Array [],
1430+
"props": Object {
1431+
"foo": Object {
1432+
"description": "",
1433+
"required": true,
1434+
"tsType": Object {
1435+
"name": "string",
1436+
},
1437+
},
1438+
},
1439+
}
1440+
`;
1441+
1442+
exports[`main fixtures processes component "component_29.tsx" without errors 1`] = `
1443+
Object {
1444+
"description": "This is a typescript component with imported prop types",
1445+
"displayName": "ImportedExtendedComponent",
1446+
"methods": Array [],
1447+
"props": Object {
1448+
"bar": Object {
1449+
"description": "",
1450+
"required": true,
1451+
"tsType": Object {
1452+
"name": "number",
1453+
},
1454+
},
1455+
"foo": Object {
1456+
"description": "",
1457+
"required": true,
1458+
"tsType": Object {
1459+
"name": "string",
1460+
},
1461+
},
1462+
},
1463+
}
1464+
`;
1465+
1466+
exports[`main fixtures processes component "component_30.js" without errors 1`] = `
1467+
Object {
1468+
"description": "",
1469+
"displayName": "CustomButton",
1470+
"methods": Array [],
1471+
"props": Object {
1472+
"children": Object {
1473+
"description": "",
1474+
"required": true,
1475+
"type": Object {
1476+
"name": "string",
1477+
},
1478+
},
1479+
"color": Object {
1480+
"description": "",
1481+
"required": false,
1482+
"type": Object {
1483+
"name": "string",
1484+
},
1485+
},
1486+
"onClick": Object {
1487+
"description": "",
1488+
"required": false,
1489+
"type": Object {
1490+
"name": "func",
1491+
},
1492+
},
1493+
"style": Object {
1494+
"description": "",
1495+
"required": false,
1496+
"type": Object {
1497+
"name": "object",
1498+
},
1499+
},
1500+
},
1501+
}
1502+
`;
1503+
1504+
exports[`main fixtures processes component "component_31.js" without errors 1`] = `
1505+
Object {
1506+
"description": "",
1507+
"displayName": "SuperCustomButton",
1508+
"methods": Array [],
1509+
"props": Object {
1510+
"children": Object {
1511+
"description": "",
1512+
"required": true,
1513+
"type": Object {
1514+
"name": "string",
1515+
},
1516+
},
1517+
"onClick": Object {
1518+
"description": "",
1519+
"required": false,
1520+
"type": Object {
1521+
"name": "func",
1522+
},
1523+
},
1524+
"style": Object {
1525+
"description": "",
1526+
"required": false,
1527+
"type": Object {
1528+
"name": "object",
1529+
},
1530+
},
1531+
},
1532+
}
1533+
`;
1534+
1535+
exports[`main fixtures processes component "component_32.js" without errors 1`] = `
1536+
Object {
1537+
"description": "",
1538+
"displayName": "SuperDuperCustomButton",
1539+
"methods": Array [],
1540+
"props": Object {
1541+
"children": Object {
1542+
"description": "",
1543+
"required": true,
1544+
"type": Object {
1545+
"name": "string",
1546+
},
1547+
},
1548+
"onClick": Object {
1549+
"description": "",
1550+
"required": false,
1551+
"type": Object {
1552+
"name": "func",
1553+
},
1554+
},
1555+
"style": Object {
1556+
"description": "",
1557+
"required": false,
1558+
"type": Object {
1559+
"name": "object",
1560+
},
1561+
},
1562+
},
1563+
}
1564+
`;

src/__tests__/fixtures/component_27.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import React, { Component } from 'react';
1010

11-
interface Props {
11+
export interface Props {
1212
foo: string
1313
}
1414

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import React, { Component } from 'react';
10+
import { Props as ImportedProps } from './component_27';
11+
12+
export default interface ExtendedProps extends ImportedProps {
13+
bar: number
14+
}
15+
16+
/**
17+
* This is a typescript component with imported prop types
18+
*/
19+
export function ImportedComponent(props: ImportedProps) {
20+
return <h1>Hello world</h1>;
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
*/
8+
9+
import React, { Component } from 'react';
10+
import ExtendedProps from './component_28';
11+
12+
/**
13+
* This is a typescript component with imported prop types
14+
*/
15+
export function ImportedExtendedComponent(props: ExtendedProps) {
16+
return <h1>Hello world</h1>;
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Button from './component_6';
2+
import PropTypes from 'prop-types';
3+
4+
export function CustomButton({color, ...otherProps}) {
5+
return <Button {...otherProps} style={{color}} />;
6+
}
7+
8+
CustomButton.propTypes = {
9+
...Button.propTypes,
10+
color: PropTypes.string
11+
};
12+
13+
export const sharedProps = Button.propTypes;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {CustomButton, sharedProps} from './component_30';
2+
import PropTypes from 'prop-types';
3+
4+
export function SuperCustomButton({color, ...otherProps}) {
5+
return <CustomButton {...otherProps} style={{color}} />;
6+
}
7+
8+
SuperCustomButton.propTypes = sharedProps;
9+
export {sharedProps};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {SuperCustomButton, sharedProps} from './component_31';
2+
import PropTypes from 'prop-types';
3+
4+
export function SuperDuperCustomButton({color, ...otherProps}) {
5+
return <SuperCustomButton {...otherProps} style={{color}} />;
6+
}
7+
8+
SuperDuperCustomButton.propTypes = sharedProps;

src/babelParser.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,17 @@ function buildOptions(
100100
export default function buildParse(options?: Options = {}): Parser {
101101
const { parserOptions, ...babelOptions } = options;
102102
const parserOpts = buildOptions(parserOptions, babelOptions);
103+
const opts = {
104+
parserOpts,
105+
...babelOptions,
106+
};
103107

104108
return {
105109
parse(src: string): ASTNode {
106-
return babel.parseSync(src, {
107-
parserOpts,
108-
...babelOptions,
109-
});
110+
const ast = babel.parseSync(src, opts);
111+
// Attach options to the Program node, for use when processing imports.
112+
ast.program.options = options;
113+
return ast;
110114
},
111115
};
112116
}

src/handlers/__tests__/propDocblockHandler-test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ describe('propDocBlockHandler', () => {
128128
const definition = parse(
129129
getSrc(
130130
`{
131-
...Foo.propTypes,
131+
...Bar.propTypes,
132132
/**
133133
* Foo comment
134134
*/

src/utils/getMemberValuePath.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ const LOOKUP_METHOD = {
3737
[t.ClassExpression.name]: getClassMemberValuePath,
3838
};
3939

40-
function isSupportedDefinitionType({ node }) {
40+
export function isSupportedDefinitionType({ node }) {
4141
return (
4242
t.ObjectExpression.check(node) ||
4343
t.ClassDeclaration.check(node) ||

src/utils/isReactComponentClass.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export default function isReactComponentClass(path: NodePath): boolean {
6464
if (!node.superClass) {
6565
return false;
6666
}
67-
const superClass = resolveToValue(path.get('superClass'));
67+
const superClass = resolveToValue(path.get('superClass'), false);
6868
if (!match(superClass.node, { property: { name: 'Component' } })) {
6969
return false;
7070
}

src/utils/resolveImportedValue.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import types from 'ast-types';
11+
import { traverseShallow } from './traverse';
12+
import resolve from 'resolve';
13+
import { dirname } from 'path';
14+
import buildParser, { type Options } from '../babelParser';
15+
import fs from 'fs';
16+
17+
const { namedTypes: t, NodePath } = types;
18+
19+
export default function resolveImportedValue(path: NodePath, name: string) {
20+
t.ImportDeclaration.assert(path.node);
21+
22+
// Bail if no filename was provided for the current source file.
23+
// Also never traverse into react itself.
24+
const source = path.node.source.value;
25+
const options = getOptions(path);
26+
if (!options || !options.filename || source === 'react') {
27+
return null;
28+
}
29+
30+
// Resolve the imported module using the Node resolver
31+
const basedir = dirname(options.filename);
32+
let resolvedSource;
33+
34+
try {
35+
resolvedSource = resolve.sync(source, {
36+
basedir,
37+
extensions: ['.js', '.jsx', '.ts', '.tsx'],
38+
});
39+
} catch (err) {
40+
return null;
41+
}
42+
43+
// Read and parse the code
44+
// TODO: cache and reuse
45+
const code = fs.readFileSync(resolvedSource, 'utf8');
46+
const parseOptions: Options = {
47+
...options,
48+
parserOptions: {},
49+
filename: resolvedSource,
50+
};
51+
52+
const parser = buildParser(parseOptions);
53+
const ast = parser.parse(code);
54+
return findExportedValue(ast.program, name);
55+
}
56+
57+
// Find the root Program node, which we attached our options too in babelParser.js
58+
function getOptions(path: NodePath): Options {
59+
while (!t.Program.check(path.node)) {
60+
path = path.parentPath;
61+
}
62+
63+
return path.node.options || {};
64+
}
65+
66+
// Traverses the program looking for an export that matches the requested name
67+
function findExportedValue(ast, name) {
68+
let resultPath: ?NodePath = null;
69+
70+
traverseShallow(ast, {
71+
visitExportNamedDeclaration(path: NodePath) {
72+
const { declaration, specifiers } = path.node;
73+
if (declaration && declaration.id && declaration.id.name === name) {
74+
resultPath = path.get('declaration');
75+
} else if (declaration && declaration.declarations) {
76+
path.get('declaration', 'declarations').each((declPath: NodePath) => {
77+
const decl = declPath.node;
78+
// TODO: ArrayPattern and ObjectPattern
79+
if (
80+
t.Identifier.check(decl.id) &&
81+
decl.id.name === name &&
82+
decl.init
83+
) {
84+
resultPath = declPath.get('init');
85+
}
86+
});
87+
} else if (specifiers) {
88+
path.get('specifiers').each((specifierPath: NodePath) => {
89+
if (specifierPath.node.exported.name === name) {
90+
resultPath = specifierPath.get('local');
91+
}
92+
});
93+
}
94+
95+
return false;
96+
},
97+
visitExportDefaultDeclaration(path: NodePath) {
98+
if (name === 'default') {
99+
resultPath = path.get('declaration');
100+
}
101+
102+
return false;
103+
},
104+
// TODO: visitExportAllDeclaration
105+
});
106+
107+
return resultPath;
108+
}

0 commit comments

Comments
 (0)