Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Commit c7df614

Browse files
committed
feat: support named imports from prop-types
1 parent 8c1f418 commit c7df614

6 files changed

+178
-23
lines changed

Diff for: src/types.ts

+24-15
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import astToCode from 'babel-generator';
22
import chalk from 'chalk';
33
import * as dom from 'dts-dom';
44
import { IOptions } from './index';
5-
import { propTypeQueryExpression, AstQuery } from './typings';
5+
import { propTypeQueryExpression, AstQuery, ImportedPropTypes } from './typings';
66

77
export interface TypeDeclaration {
88
type: any;
@@ -16,14 +16,14 @@ function getTypeDeclaration(type: any, optional: boolean): TypeDeclaration {
1616
};
1717
}
1818

19-
export function get(ast: AstQuery, propertyAst: any, propTypesName: string|undefined,
19+
export function get(ast: AstQuery, propertyAst: any, importedPropTypes: ImportedPropTypes,
2020
options: IOptions): TypeDeclaration {
2121
try {
22-
const simpleType = getSimpleType(ast, propertyAst, propTypesName);
22+
const simpleType = getSimpleType(ast, propertyAst, importedPropTypes);
2323
if (simpleType) {
2424
return simpleType;
2525
}
26-
const complexType = getComplexType(ast, propertyAst, propTypesName, options);
26+
const complexType = getComplexType(ast, propertyAst, importedPropTypes, options);
2727
if (complexType) {
2828
return complexType;
2929
}
@@ -43,8 +43,8 @@ export function get(ast: AstQuery, propertyAst: any, propTypesName: string|undef
4343

4444
// tslint:disable:next-line cyclomatic-complexity
4545
function getSimpleType(ast: AstQuery, propertyAst: any,
46-
propTypesName: string|undefined): TypeDeclaration|undefined {
47-
const [required, simpleTypeName] = getSimpleTypeName(ast, propertyAst, propTypesName);
46+
importedPropTypes: ImportedPropTypes): TypeDeclaration|undefined {
47+
const [required, simpleTypeName] = getSimpleTypeName(ast, propertyAst, importedPropTypes);
4848
switch (simpleTypeName) {
4949
case 'any':
5050
return getTypeDeclaration('any', !required);
@@ -75,18 +75,18 @@ function getSimpleType(ast: AstQuery, propertyAst: any,
7575
}
7676

7777
function getComplexType(ast: AstQuery, propertyAst: any,
78-
propTypesName: string|undefined, options: IOptions): TypeDeclaration|undefined {
79-
const [required, complexTypeName, typeAst] = getComplexTypeName(ast, propertyAst, propTypesName);
78+
importedPropTypes: ImportedPropTypes, options: IOptions): TypeDeclaration|undefined {
79+
const [required, complexTypeName, typeAst] = getComplexTypeName(ast, propertyAst, importedPropTypes);
8080
switch (complexTypeName) {
8181
case 'instanceOf':
8282
return getTypeDeclaration(dom.create.typeof(
8383
dom.create.namedTypeReference(typeAst.arguments[0].name)), !required);
8484
case 'oneOfType':
8585
const typeDecls = typeAst.arguments[0].elements
86-
.map((subtree: any) => get(ast, subtree, propTypesName, options)) as TypeDeclaration[];
86+
.map((subtree: any) => get(ast, subtree, importedPropTypes, options)) as TypeDeclaration[];
8787
return getTypeDeclaration(dom.create.union(typeDecls.map(type => type.type)), !required);
8888
case 'arrayOf':
89-
const typeDecl = get(ast, typeAst.arguments[0], propTypesName, options);
89+
const typeDecl = get(ast, typeAst.arguments[0], importedPropTypes, options);
9090
return getTypeDeclaration(dom.create.array(typeDecl.type), !required);
9191
case 'oneOf':
9292
// tslint:disable:next-line comment-format
@@ -95,7 +95,7 @@ function getComplexType(ast: AstQuery, propertyAst: any,
9595
return getTypeDeclaration(dom.create.union(enumEntries as dom.Type[]), !required);
9696
case 'shape':
9797
const entries = getShapeProperties(ast, typeAst.arguments[0]).map((entry: any) => {
98-
const typeDecl = get(ast, entry.value, propTypesName, options);
98+
const typeDecl = get(ast, entry.value, importedPropTypes, options);
9999
return dom.create.property(entry.key.name, typeDecl.type,
100100
typeDecl.optional ? dom.DeclarationFlags.Optional : dom.DeclarationFlags.None);
101101
});
@@ -115,23 +115,32 @@ function isRequired(ast: AstQuery, propertyAst: any): [boolean, any] {
115115
}
116116

117117
function getSimpleTypeName(ast: AstQuery, propertyAst: any,
118-
propTypesName: string|undefined): [boolean, string|undefined] {
118+
importedPropTypes: ImportedPropTypes): [boolean, string|undefined] {
119+
const {propTypesName, propTypes} = importedPropTypes;
119120
const [required, typeAst] = isRequired(ast, propertyAst);
121+
122+
if (!propTypesName && typeAst.type === 'Identifier') {
123+
const propType = propTypes.find(({localName}) => localName === typeAst.name);
124+
return [required, propType ? propType.importedName : undefined];
125+
}
126+
120127
const res = ast.querySubtree(typeAst, `
121128
MemberExpression[
122129
(${propTypeQueryExpression(propTypesName)})
123130
&&
124131
/:property Identifier
125132
]
133+
/:property Identifier
126134
`);
127-
return [required, res.length > 0 ? res[0].property.name : undefined];
135+
136+
return [required, res.length > 0 ? res[0].name : undefined];
128137
}
129138

130139
function getComplexTypeName(ast: AstQuery, propertyAst: any,
131-
propTypesName: string|undefined): [boolean, string|undefined, any] {
140+
importedPropTypes: ImportedPropTypes): [boolean, string|undefined, any] {
132141
const [required, typeAst] = isRequired(ast, propertyAst);
133142
if (typeAst.type === 'CallExpression') {
134-
const [, simpleTypeName] = getSimpleTypeName(ast, typeAst.callee, propTypesName);
143+
const [, simpleTypeName] = getSimpleTypeName(ast, typeAst.callee, importedPropTypes);
135144
return [required, simpleTypeName, typeAst];
136145
}
137146
return [required, undefined, typeAst];

Diff for: src/typings.ts

+41-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ export interface AstQuery {
1010
querySubtree(subtree: any, query: string): any[];
1111
}
1212

13+
export interface ImportedPropType {
14+
importedName: string;
15+
localName: string;
16+
}
17+
18+
export interface ImportedPropTypes {
19+
propTypesName: string|undefined;
20+
propTypes: ImportedPropType[];
21+
}
22+
1323
export function createTypings(moduleName: string|null, programAst: any, options: IOptions,
1424
reactImport: string): string {
1525
const astq = new ASTQ();
@@ -23,8 +33,11 @@ export function createTypings(moduleName: string|null, programAst: any, options:
2333
}
2434
};
2535
const reactComponentName = getReactComponentName(ast);
26-
const propTypesName = getPropTypesName(ast);
27-
const importedTypes = getInstanceOfPropTypes(ast, propTypesName);
36+
const importedPropTypes: ImportedPropTypes = {
37+
propTypesName: getPropTypesName(ast),
38+
propTypes: getImportedPropTypes(ast)
39+
};
40+
const importedTypes = getInstanceOfPropTypes(ast, importedPropTypes);
2841
const importStatements = getImportStatements(ast, importedTypes, options.instanceOfResolver);
2942
const componentNames = getUniqueNames([
3043
...getComponentNamesByPropTypeAssignment(ast),
@@ -49,7 +62,8 @@ export function createTypings(moduleName: string|null, programAst: any, options:
4962
const exportType = getComponentExportType(ast, componentName);
5063
const propTypes = getPropTypes(ast, componentName);
5164
if (exportType) {
52-
createExportedTypes(m, ast, componentName, reactComponentName, propTypes, propTypesName, exportType, options);
65+
createExportedTypes(m, ast, componentName, reactComponentName, propTypes, importedPropTypes, exportType,
66+
options);
5367
}
5468
});
5569

@@ -63,14 +77,14 @@ export function createTypings(moduleName: string|null, programAst: any, options:
6377
}
6478

6579
function createExportedTypes(m: dom.ModuleDeclaration, ast: AstQuery, componentName: string,
66-
reactComponentName: string|undefined, propTypes: any, propTypesName: string|undefined,
80+
reactComponentName: string|undefined, propTypes: any, importedPropTypes: ImportedPropTypes,
6781
exportType: dom.DeclarationFlags, options: IOptions): void {
6882
const classComponent = isClassComponent(ast, componentName, reactComponentName);
6983

7084
const interf = dom.create.interface(`${componentName}Props`);
7185
interf.flags = dom.DeclarationFlags.Export;
7286
if (propTypes) {
73-
createPropTypeTypings(interf, ast, propTypes, propTypesName, options);
87+
createPropTypeTypings(interf, ast, propTypes, importedPropTypes, options);
7488
extractComplexTypes(m, interf, componentName);
7589
}
7690

@@ -93,12 +107,12 @@ function createExportedTypes(m: dom.ModuleDeclaration, ast: AstQuery, componentN
93107
}
94108

95109
function createPropTypeTypings(interf: dom.InterfaceDeclaration, ast: AstQuery, propTypes: any,
96-
propTypesName: string|undefined, options: IOptions): void {
110+
importedPropTypes: ImportedPropTypes, options: IOptions): void {
97111
const res = ast.querySubtree(propTypes, `
98112
/ ObjectProperty
99113
`);
100114
res.forEach(propertyAst => {
101-
const typeDecl = types.get(ast, propertyAst.value, propTypesName, options);
115+
const typeDecl = types.get(ast, propertyAst.value, importedPropTypes, options);
102116
const property = dom.create.property(propertyAst.key.name || propertyAst.key.value, typeDecl.type,
103117
typeDecl.optional ? dom.DeclarationFlags.Optional : 0);
104118
if (propertyAst.leadingComments && propertyAst.leadingComments[0].type === 'CommentBlock') {
@@ -231,6 +245,18 @@ function getPropTypesName(ast: AstQuery): string|undefined {
231245
return undefined;
232246
}
233247

248+
function getImportedPropTypes(ast: AstQuery): ImportedPropType[] {
249+
return ast.query(`
250+
// ImportDeclaration[
251+
/:source StringLiteral[@value == 'prop-types']
252+
]
253+
/:specifiers ImportSpecifier
254+
`).map(({imported, local}) => ({
255+
importedName: imported.name,
256+
localName: local.name
257+
}));
258+
}
259+
234260
function hasReactClass(ast: AstQuery, reactComponentName: string|undefined): boolean {
235261
const res = ast.query(`
236262
// ClassDeclaration[
@@ -261,17 +287,24 @@ function hasReactClass(ast: AstQuery, reactComponentName: string|undefined): boo
261287
return res.length > 0;
262288
}
263289

264-
function getInstanceOfPropTypes(ast: AstQuery, propTypesName: string|undefined): string[] {
290+
function getInstanceOfPropTypes(ast: AstQuery, importedPropTypes: ImportedPropTypes): string[] {
291+
const {propTypesName, propTypes} = importedPropTypes;
292+
const instanceOfPropType = propTypes.find(({importedName}) => importedName === 'instanceOf');
293+
const localInstanceOfName = instanceOfPropType ? instanceOfPropType.localName : undefined;
294+
265295
const res = ast.query(`
266296
// CallExpression[
267297
/:callee MemberExpression[
268298
(${propTypeQueryExpression(propTypesName)})
269299
&&
270300
/:property Identifier[@name == 'instanceOf']
271301
]
302+
||
303+
/:callee Identifier[@name == '${localInstanceOfName}']
272304
]
273305
/:arguments *
274306
`);
307+
275308
return res.map(identifer => identifer.name);
276309
}
277310

Diff for: tests/es7-class-babeled-to-es6.d.ts

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
declare module 'component' {
2+
import {Component} from 'react';
3+
4+
import Message from './path/to/Message';
5+
6+
export type MyComponentOptionalEnum = 'News' | 'Photos' | 1 | 2;
7+
8+
export type MyComponentOptionalUnion = string | number;
9+
10+
export interface MyComponentOptionalObjectWithShape {
11+
color?: string;
12+
fontSize?: number;
13+
}
14+
15+
export type MyComponentRequiredUnion = any[] | boolean;
16+
17+
export interface MyComponentRequiredArrayOfObjectsWithShape {
18+
color?: string;
19+
fontSize?: number;
20+
}
21+
22+
export interface MyComponentDeeplyNested {
23+
arrayInDeeplyNested?: {
24+
foo?: number;
25+
}[];
26+
}
27+
28+
export interface MyComponentProps {
29+
/**
30+
* This is a jsdoc comment for optionalAny.
31+
*/
32+
optionalAny?: any;
33+
optionalArray?: any[];
34+
optionalBool?: boolean;
35+
optionalFunc?: (...args: any[]) => any;
36+
optionalNumber?: number;
37+
optionalObject?: Object;
38+
optionalString?: string;
39+
optionalNode?: React.ReactNode;
40+
optionalElement?: React.ReactElement<any>;
41+
optionalMessage?: typeof Message;
42+
optionalEnum?: MyComponentOptionalEnum;
43+
optionalUnion?: MyComponentOptionalUnion;
44+
optionalArrayOf?: number[];
45+
optionalObjectWithShape?: MyComponentOptionalObjectWithShape;
46+
requiredFunc: (...args: any[]) => any;
47+
requiredAny: any;
48+
requiredUnion: MyComponentRequiredUnion;
49+
requiredArrayOf: string[];
50+
requiredArrayOfObjectsWithShape: MyComponentRequiredArrayOfObjectsWithShape[];
51+
deeplyNested: MyComponentDeeplyNested[];
52+
requiredSymbol: typeof Symbol;
53+
}
54+
55+
export class MyComponent extends Component<MyComponentProps, any> {
56+
render(): JSX.Element;
57+
}
58+
}

Diff for: tests/es7-class-babeled-to-es6.js

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Component, createElement } from 'react';
2+
import { any, array, arrayOf, bool, element, func, instanceOf, node, number, object, oneOf, oneOfType, shape, string, symbol } from 'prop-types';
3+
import Message from './path/to/Message';
4+
5+
class MyComponent extends Component {
6+
7+
render() {
8+
return createElement('div', null);
9+
}
10+
}
11+
MyComponent.propTypes = {
12+
/**
13+
* This is a jsdoc comment for optionalAny.
14+
*/
15+
optionalAny: any,
16+
optionalArray: array,
17+
optionalBool: bool,
18+
optionalFunc: func,
19+
optionalNumber: number,
20+
optionalObject: object,
21+
optionalString: string,
22+
optionalNode: node,
23+
optionalElement: element,
24+
optionalMessage: instanceOf(Message),
25+
optionalEnum: oneOf(['News', 'Photos', 1, 2]),
26+
optionalUnion: oneOfType([string, number]),
27+
optionalArrayOf: arrayOf(number),
28+
optionalObjectWithShape: shape({
29+
color: string,
30+
fontSize: number
31+
}),
32+
requiredFunc: func.isRequired,
33+
requiredAny: any.isRequired,
34+
requiredUnion: oneOfType([array, bool]).isRequired,
35+
requiredArrayOf: arrayOf(string).isRequired,
36+
requiredArrayOfObjectsWithShape: arrayOf(shape({
37+
color: string,
38+
fontSize: number
39+
})).isRequired,
40+
deeplyNested: arrayOf(shape({
41+
arrayInDeeplyNested: arrayOf(shape({
42+
foo: number
43+
}))
44+
})).isRequired,
45+
requiredSymbol: symbol.isRequired
46+
};
47+
48+
export { MyComponent };

Diff for: tests/parsing-test.ts

+6
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ test('Parsing should create definition from babeled es7 class component', t => {
6666
};
6767
compare(t, 'component', 'es7-class-babeled.js', 'es7-class.d.ts', opts);
6868
});
69+
test('Parsing should create definition from es7 class component babeled to es6', t => {
70+
const opts: react2dts.IOptions = {
71+
instanceOfResolver: (): string => './path/to/Message'
72+
};
73+
compare(t, 'component', 'es7-class-babeled-to-es6.js', 'es7-class-babeled-to-es6.d.ts', opts);
74+
});
6975
test('Parsing should create definition from es7 class component with separate default export', t => {
7076
compare(t, 'component', 'es7-class-separate-export.jsx', 'es7-class-separate-export.d.ts');
7177
});

Diff for: tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"lib": [
66
"dom",
77
"es5",
8+
"es2015.core",
89
"es2015.iterable",
910
"es2015.promise"
1011
],

0 commit comments

Comments
 (0)