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

Commit c7a988b

Browse files
authored
feat: add support for stateless function components (#204)
* feat: add support for stateless function components stateless components for react are a performance optimization we should be able to create proper propTypes for it
1 parent 594e175 commit c7a988b

11 files changed

+499
-390
lines changed

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"typescript": "2.0.6"
5858
},
5959
"dependencies": {
60+
"astq": "1.8.0",
6061
"babylon": "6.13.1",
6162
"dts-dom": "0.1.11",
6263
"minimist": "1.2.0"

Diff for: src/analyzer.ts

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import * as dom from 'dts-dom';
2+
import * as astqts from 'astq';
3+
const ASTQ: typeof astqts.ASTQ = astqts as any;
4+
5+
import { InstanceOfResolver, IPropTypes, IProp, IASTNode } from './index';
6+
7+
const defaultInstanceOfResolver: InstanceOfResolver = (_name: string): undefined => undefined;
8+
9+
export function parsePropTypes(node: any, instanceOfResolver?: InstanceOfResolver): IPropTypes {
10+
const astq = new ASTQ();
11+
return astq
12+
.query(node, `/ObjectProperty`)
13+
.reduce((propTypes: IPropTypes, propertyNode: IASTNode) => {
14+
const prop: IProp = getTypeFromPropType(propertyNode.value, instanceOfResolver);
15+
prop.documentation = getOptionalDocumentation(propertyNode);
16+
propTypes[propertyNode.key.name] = prop;
17+
return propTypes;
18+
}, {});
19+
}
20+
21+
function getOptionalDocumentation(propertyNode: any): string {
22+
return (((propertyNode.leadingComments || []) as any[])
23+
.filter(comment => comment.type == 'CommentBlock')[0] || {})
24+
.value;
25+
}
26+
27+
/**
28+
* @internal
29+
*/
30+
export function getTypeFromPropType(node: IASTNode, instanceOfResolver = defaultInstanceOfResolver): IProp {
31+
const result: IProp = {
32+
type: 'any',
33+
type2: 'any',
34+
optional: true
35+
};
36+
if (isNode(node)) {
37+
const {isRequired, type} = isRequiredPropType(node, instanceOfResolver);
38+
result.optional = !isRequired;
39+
switch (type.name) {
40+
case 'any':
41+
result.type = 'any';
42+
result.type2 = 'any';
43+
break;
44+
case 'array':
45+
result.type = (type.arrayType || 'any') + '[]';
46+
result.type2 = dom.create.array(type.arrayType2 || 'any');
47+
break;
48+
case 'bool':
49+
result.type = 'boolean';
50+
result.type2 = 'boolean';
51+
break;
52+
case 'func':
53+
result.type = '(...args: any[]) => any';
54+
result.type2 = dom.create.functionType([
55+
dom.create.parameter('args', dom.create.array('any'), dom.ParameterFlags.Rest)], 'any');
56+
break;
57+
case 'number':
58+
result.type = 'number';
59+
result.type2 = 'number';
60+
break;
61+
case 'object':
62+
result.type = 'Object';
63+
result.type2 = dom.create.namedTypeReference('Object');
64+
break;
65+
case 'string':
66+
result.type = 'string';
67+
result.type2 = 'string';
68+
break;
69+
case 'node':
70+
result.type = 'React.ReactNode';
71+
result.type2 = dom.create.namedTypeReference('React.ReactNode');
72+
break;
73+
case 'element':
74+
result.type = 'React.ReactElement<any>';
75+
result.type2 = dom.create.namedTypeReference('React.ReactElement<any>');
76+
break;
77+
case 'union':
78+
result.type = type.types.map((unionType: string) => unionType).join('|');
79+
result.type2 = dom.create.union(type.types2);
80+
break;
81+
case 'instanceOf':
82+
if (type.importPath) {
83+
result.type = 'typeof ' + type.type;
84+
result.type2 = dom.create.typeof(type.type2);
85+
(result as any).importType = type.type;
86+
(result as any).importPath = type.importPath;
87+
} else {
88+
result.type = 'any';
89+
result.type2 = 'any';
90+
}
91+
break;
92+
}
93+
}
94+
return result;
95+
}
96+
97+
function isNode(obj: IASTNode): boolean {
98+
return obj && typeof obj.type != 'undefined' && typeof obj.loc != 'undefined';
99+
}
100+
101+
function getReactPropTypeFromExpression(node: any, instanceOfResolver: InstanceOfResolver): any {
102+
if (node.type == 'MemberExpression' && node.object.type == 'MemberExpression'
103+
&& node.object.object.name == 'React' && node.object.property.name == 'PropTypes') {
104+
return node.property;
105+
} else if (node.type == 'CallExpression') {
106+
const callType = getReactPropTypeFromExpression(node.callee, instanceOfResolver);
107+
switch (callType.name) {
108+
case 'instanceOf':
109+
return {
110+
name: 'instanceOf',
111+
type: node.arguments[0].name,
112+
type2: dom.create.namedTypeReference(node.arguments[0].name),
113+
importPath: instanceOfResolver(node.arguments[0].name)
114+
};
115+
case 'arrayOf':
116+
const arrayType = getTypeFromPropType(node.arguments[0], instanceOfResolver);
117+
return {
118+
name: 'array',
119+
arrayType: arrayType.type,
120+
arrayType2: arrayType.type2
121+
};
122+
case 'oneOfType':
123+
const unionTypes = node.arguments[0].elements.map((element: IASTNode) =>
124+
getTypeFromPropType(element, instanceOfResolver));
125+
return {
126+
name: 'union',
127+
types: unionTypes.map((type: any) => type.type),
128+
types2: unionTypes.map((type: any) => type.type2)
129+
};
130+
}
131+
}
132+
return 'undefined';
133+
}
134+
135+
function isRequiredPropType(node: any, instanceOfResolver: InstanceOfResolver): any {
136+
const isRequired = node.type == 'MemberExpression' && node.property.name == 'isRequired';
137+
return {
138+
isRequired,
139+
type: getReactPropTypeFromExpression(isRequired ? node.object : node, instanceOfResolver)
140+
};
141+
}

Diff for: src/generate-typigns.ts

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as dom from 'dts-dom';
2+
3+
import { IParsingResult, IPropTypes, ExportType } from './index';
4+
5+
export function generateTypings(moduleName: string|null, parsingResult: IParsingResult): string {
6+
const {exportType, classname, functionname, propTypes} = parsingResult;
7+
const componentName = classname || functionname || 'Anonymous';
8+
9+
const m = dom.create.module(moduleName || 'moduleName');
10+
if (classname) {
11+
m.members.push(dom.create.importAll('React', 'react'));
12+
}
13+
if (propTypes) {
14+
Object.keys(propTypes).forEach(propName => {
15+
const prop = propTypes[propName];
16+
if (prop.importType && prop.importPath) {
17+
m.members.push(dom.create.importDefault(prop.importType, prop.importPath));
18+
}
19+
});
20+
}
21+
const interf = createReactPropInterface(componentName, propTypes);
22+
m.members.push(interf);
23+
24+
if (classname) {
25+
m.members.push(createReactClassDeclaration(componentName, exportType, propTypes, interf));
26+
} else if (functionname) {
27+
m.members.push(createReactFunctionDeclaration(componentName, exportType, interf));
28+
}
29+
30+
if (moduleName === null) {
31+
return m.members
32+
.map(member => dom.emit(member, dom.ContextFlags.None))
33+
.join('');
34+
} else {
35+
return dom.emit(m, dom.ContextFlags.Module);
36+
}
37+
}
38+
39+
function createReactPropInterface(componentName: string, propTypes: IPropTypes): dom.InterfaceDeclaration {
40+
const interf = dom.create.interface(`${componentName}Props`);
41+
interf.flags = dom.DeclarationFlags.Export;
42+
Object.keys(propTypes).forEach(propName => {
43+
const prop = propTypes[propName];
44+
45+
const property = dom.create.property(propName, prop.type2,
46+
prop.optional ? dom.DeclarationFlags.Optional : 0);
47+
if (prop.documentation) {
48+
property.jsDocComment = prop.documentation
49+
.split('\n')
50+
.map(line => line.trim())
51+
.map(line => line.replace(/^\*\*?/, ''))
52+
.map(line => line.trim())
53+
.filter(trimLines())
54+
.reverse()
55+
.filter(trimLines())
56+
.reverse()
57+
.join('\n');
58+
}
59+
interf.members.push(property);
60+
});
61+
return interf;
62+
}
63+
64+
function trimLines(): (line: string) => boolean {
65+
let characterFound = false;
66+
return (line: string) => (characterFound = Boolean(line)) && Boolean(line);
67+
}
68+
69+
function createReactClassDeclaration(componentName: string, exportType: ExportType, propTypes: IPropTypes,
70+
interf: dom.InterfaceDeclaration): dom.ClassDeclaration {
71+
const classDecl = dom.create.class(componentName);
72+
classDecl.baseType = dom.create.interface(`React.Component<${propTypes ? interf.name : 'any'}, any>`);
73+
classDecl.flags = exportType === ExportType.default ?
74+
dom.DeclarationFlags.ExportDefault :
75+
dom.DeclarationFlags.Export;
76+
return classDecl;
77+
}
78+
79+
function createReactFunctionDeclaration(componentName: string, exportType: ExportType,
80+
interf: dom.InterfaceDeclaration): dom.FunctionDeclaration {
81+
const funcDelc = dom.create.function(componentName, [dom.create.parameter('props', interf)],
82+
dom.create.namedTypeReference('JSX.Element'));
83+
funcDelc.flags = exportType === ExportType.default ?
84+
dom.DeclarationFlags.ExportDefault :
85+
dom.DeclarationFlags.Export;
86+
return funcDelc;
87+
}

Diff for: src/generator.ts

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { IPropTypes, ExportType } from './index';
2+
3+
export class Generator {
4+
5+
private static NL: string = '\n';
6+
7+
private indentLevel: number = 0;
8+
9+
private code: string = '';
10+
11+
private indent(): void {
12+
let result = '';
13+
for (let i = 0, n = this.indentLevel; i < n; i++) {
14+
result += '\t';
15+
}
16+
this.code += result;
17+
}
18+
19+
public nl(): void {
20+
this.code += Generator.NL;
21+
}
22+
23+
public declareModule(name: string, fn: () => void): void {
24+
this.indent();
25+
this.code += `declare module '${name}' {`;
26+
this.nl();
27+
this.indentLevel++;
28+
fn();
29+
this.indentLevel--;
30+
this.indent();
31+
this.code += '}';
32+
this.nl();
33+
}
34+
35+
public import(decl: string, from: string, fn?: () => void): void {
36+
this.indent();
37+
this.code += `import ${decl} from '${from}';`;
38+
this.nl();
39+
if (fn) {
40+
fn();
41+
}
42+
}
43+
44+
public props(name: string, props: IPropTypes, fn?: () => void): void {
45+
this.interface(`${name}Props`, () => {
46+
Object.keys(props).forEach(propName => {
47+
const prop = props[propName];
48+
this.prop(propName, prop.type, prop.optional, prop.documentation);
49+
});
50+
});
51+
if (fn) {
52+
fn();
53+
}
54+
}
55+
56+
public prop(name: string, type: string, optional: boolean, documentation?: string): void {
57+
this.indent();
58+
if (documentation) {
59+
this.comment(documentation);
60+
}
61+
this.code += `${name}${optional ? '?' : ''}: ${type};`;
62+
this.nl();
63+
}
64+
65+
public comment(comment: string): void {
66+
this.code += '/*';
67+
const lines = (comment || '').replace(/\t/g, '').split(/\n/g);
68+
lines.forEach((line, index) => {
69+
this.code += line;
70+
if (index < lines.length - 1) {
71+
this.nl();
72+
this.indent();
73+
}
74+
});
75+
this.code += '*/';
76+
this.nl();
77+
this.indent();
78+
}
79+
80+
public interface(name: string, fn: () => void): void {
81+
this.indent();
82+
this.code += `export interface ${name} {`;
83+
this.nl();
84+
this.indentLevel++;
85+
fn();
86+
this.indentLevel--;
87+
this.indent();
88+
this.code += '}';
89+
this.nl();
90+
}
91+
92+
public exportDeclaration(exportType: ExportType, fn: () => void): void {
93+
this.indent();
94+
this.code += 'export ';
95+
if (exportType == ExportType.default) {
96+
this.code += 'default ';
97+
}
98+
fn();
99+
}
100+
101+
public class(name: string, props: boolean, fn?: () => void): void {
102+
this.code += `class ${name} extends React.Component<${props ? `${name}Props` : 'any'}, any> {`;
103+
this.nl();
104+
this.indentLevel++;
105+
if (fn) {
106+
fn();
107+
}
108+
this.indentLevel--;
109+
this.indent();
110+
this.code += '}';
111+
this.nl();
112+
}
113+
114+
public toString(): string {
115+
return this.code;
116+
}
117+
118+
}

0 commit comments

Comments
 (0)