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

Commit

Permalink
feat: add support for stateless function components (#204)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
KnisterPeter committed Oct 31, 2016
1 parent 594e175 commit c7a988b
Show file tree
Hide file tree
Showing 11 changed files with 499 additions and 390 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"typescript": "2.0.6"
},
"dependencies": {
"astq": "1.8.0",
"babylon": "6.13.1",
"dts-dom": "0.1.11",
"minimist": "1.2.0"
Expand Down
141 changes: 141 additions & 0 deletions src/analyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import * as dom from 'dts-dom';
import * as astqts from 'astq';
const ASTQ: typeof astqts.ASTQ = astqts as any;

import { InstanceOfResolver, IPropTypes, IProp, IASTNode } from './index';

const defaultInstanceOfResolver: InstanceOfResolver = (_name: string): undefined => undefined;

export function parsePropTypes(node: any, instanceOfResolver?: InstanceOfResolver): IPropTypes {
const astq = new ASTQ();
return astq
.query(node, `/ObjectProperty`)
.reduce((propTypes: IPropTypes, propertyNode: IASTNode) => {
const prop: IProp = getTypeFromPropType(propertyNode.value, instanceOfResolver);
prop.documentation = getOptionalDocumentation(propertyNode);
propTypes[propertyNode.key.name] = prop;
return propTypes;
}, {});
}

function getOptionalDocumentation(propertyNode: any): string {
return (((propertyNode.leadingComments || []) as any[])
.filter(comment => comment.type == 'CommentBlock')[0] || {})
.value;
}

/**
* @internal
*/
export function getTypeFromPropType(node: IASTNode, instanceOfResolver = defaultInstanceOfResolver): IProp {
const result: IProp = {
type: 'any',
type2: 'any',
optional: true
};
if (isNode(node)) {
const {isRequired, type} = isRequiredPropType(node, instanceOfResolver);
result.optional = !isRequired;
switch (type.name) {
case 'any':
result.type = 'any';
result.type2 = 'any';
break;
case 'array':
result.type = (type.arrayType || 'any') + '[]';
result.type2 = dom.create.array(type.arrayType2 || 'any');
break;
case 'bool':
result.type = 'boolean';
result.type2 = 'boolean';
break;
case 'func':
result.type = '(...args: any[]) => any';
result.type2 = dom.create.functionType([
dom.create.parameter('args', dom.create.array('any'), dom.ParameterFlags.Rest)], 'any');
break;
case 'number':
result.type = 'number';
result.type2 = 'number';
break;
case 'object':
result.type = 'Object';
result.type2 = dom.create.namedTypeReference('Object');
break;
case 'string':
result.type = 'string';
result.type2 = 'string';
break;
case 'node':
result.type = 'React.ReactNode';
result.type2 = dom.create.namedTypeReference('React.ReactNode');
break;
case 'element':
result.type = 'React.ReactElement<any>';
result.type2 = dom.create.namedTypeReference('React.ReactElement<any>');
break;
case 'union':
result.type = type.types.map((unionType: string) => unionType).join('|');
result.type2 = dom.create.union(type.types2);
break;
case 'instanceOf':
if (type.importPath) {
result.type = 'typeof ' + type.type;
result.type2 = dom.create.typeof(type.type2);
(result as any).importType = type.type;
(result as any).importPath = type.importPath;
} else {
result.type = 'any';
result.type2 = 'any';
}
break;
}
}
return result;
}

function isNode(obj: IASTNode): boolean {
return obj && typeof obj.type != 'undefined' && typeof obj.loc != 'undefined';
}

function getReactPropTypeFromExpression(node: any, instanceOfResolver: InstanceOfResolver): any {
if (node.type == 'MemberExpression' && node.object.type == 'MemberExpression'
&& node.object.object.name == 'React' && node.object.property.name == 'PropTypes') {
return node.property;
} else if (node.type == 'CallExpression') {
const callType = getReactPropTypeFromExpression(node.callee, instanceOfResolver);
switch (callType.name) {
case 'instanceOf':
return {
name: 'instanceOf',
type: node.arguments[0].name,
type2: dom.create.namedTypeReference(node.arguments[0].name),
importPath: instanceOfResolver(node.arguments[0].name)
};
case 'arrayOf':
const arrayType = getTypeFromPropType(node.arguments[0], instanceOfResolver);
return {
name: 'array',
arrayType: arrayType.type,
arrayType2: arrayType.type2
};
case 'oneOfType':
const unionTypes = node.arguments[0].elements.map((element: IASTNode) =>
getTypeFromPropType(element, instanceOfResolver));
return {
name: 'union',
types: unionTypes.map((type: any) => type.type),
types2: unionTypes.map((type: any) => type.type2)
};
}
}
return 'undefined';
}

function isRequiredPropType(node: any, instanceOfResolver: InstanceOfResolver): any {
const isRequired = node.type == 'MemberExpression' && node.property.name == 'isRequired';
return {
isRequired,
type: getReactPropTypeFromExpression(isRequired ? node.object : node, instanceOfResolver)
};
}
87 changes: 87 additions & 0 deletions src/generate-typigns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import * as dom from 'dts-dom';

import { IParsingResult, IPropTypes, ExportType } from './index';

export function generateTypings(moduleName: string|null, parsingResult: IParsingResult): string {
const {exportType, classname, functionname, propTypes} = parsingResult;
const componentName = classname || functionname || 'Anonymous';

const m = dom.create.module(moduleName || 'moduleName');
if (classname) {
m.members.push(dom.create.importAll('React', 'react'));
}
if (propTypes) {
Object.keys(propTypes).forEach(propName => {
const prop = propTypes[propName];
if (prop.importType && prop.importPath) {
m.members.push(dom.create.importDefault(prop.importType, prop.importPath));
}
});
}
const interf = createReactPropInterface(componentName, propTypes);
m.members.push(interf);

if (classname) {
m.members.push(createReactClassDeclaration(componentName, exportType, propTypes, interf));
} else if (functionname) {
m.members.push(createReactFunctionDeclaration(componentName, exportType, interf));
}

if (moduleName === null) {
return m.members
.map(member => dom.emit(member, dom.ContextFlags.None))
.join('');
} else {
return dom.emit(m, dom.ContextFlags.Module);
}
}

function createReactPropInterface(componentName: string, propTypes: IPropTypes): dom.InterfaceDeclaration {
const interf = dom.create.interface(`${componentName}Props`);
interf.flags = dom.DeclarationFlags.Export;
Object.keys(propTypes).forEach(propName => {
const prop = propTypes[propName];

const property = dom.create.property(propName, prop.type2,
prop.optional ? dom.DeclarationFlags.Optional : 0);
if (prop.documentation) {
property.jsDocComment = prop.documentation
.split('\n')
.map(line => line.trim())
.map(line => line.replace(/^\*\*?/, ''))
.map(line => line.trim())
.filter(trimLines())
.reverse()
.filter(trimLines())
.reverse()
.join('\n');
}
interf.members.push(property);
});
return interf;
}

function trimLines(): (line: string) => boolean {
let characterFound = false;
return (line: string) => (characterFound = Boolean(line)) && Boolean(line);
}

function createReactClassDeclaration(componentName: string, exportType: ExportType, propTypes: IPropTypes,
interf: dom.InterfaceDeclaration): dom.ClassDeclaration {
const classDecl = dom.create.class(componentName);
classDecl.baseType = dom.create.interface(`React.Component<${propTypes ? interf.name : 'any'}, any>`);
classDecl.flags = exportType === ExportType.default ?
dom.DeclarationFlags.ExportDefault :
dom.DeclarationFlags.Export;
return classDecl;
}

function createReactFunctionDeclaration(componentName: string, exportType: ExportType,
interf: dom.InterfaceDeclaration): dom.FunctionDeclaration {
const funcDelc = dom.create.function(componentName, [dom.create.parameter('props', interf)],
dom.create.namedTypeReference('JSX.Element'));
funcDelc.flags = exportType === ExportType.default ?
dom.DeclarationFlags.ExportDefault :
dom.DeclarationFlags.Export;
return funcDelc;
}
118 changes: 118 additions & 0 deletions src/generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { IPropTypes, ExportType } from './index';

export class Generator {

private static NL: string = '\n';

private indentLevel: number = 0;

private code: string = '';

private indent(): void {
let result = '';
for (let i = 0, n = this.indentLevel; i < n; i++) {
result += '\t';
}
this.code += result;
}

public nl(): void {
this.code += Generator.NL;
}

public declareModule(name: string, fn: () => void): void {
this.indent();
this.code += `declare module '${name}' {`;
this.nl();
this.indentLevel++;
fn();
this.indentLevel--;
this.indent();
this.code += '}';
this.nl();
}

public import(decl: string, from: string, fn?: () => void): void {
this.indent();
this.code += `import ${decl} from '${from}';`;
this.nl();
if (fn) {
fn();
}
}

public props(name: string, props: IPropTypes, fn?: () => void): void {
this.interface(`${name}Props`, () => {
Object.keys(props).forEach(propName => {
const prop = props[propName];
this.prop(propName, prop.type, prop.optional, prop.documentation);
});
});
if (fn) {
fn();
}
}

public prop(name: string, type: string, optional: boolean, documentation?: string): void {
this.indent();
if (documentation) {
this.comment(documentation);
}
this.code += `${name}${optional ? '?' : ''}: ${type};`;
this.nl();
}

public comment(comment: string): void {
this.code += '/*';
const lines = (comment || '').replace(/\t/g, '').split(/\n/g);
lines.forEach((line, index) => {
this.code += line;
if (index < lines.length - 1) {
this.nl();
this.indent();
}
});
this.code += '*/';
this.nl();
this.indent();
}

public interface(name: string, fn: () => void): void {
this.indent();
this.code += `export interface ${name} {`;
this.nl();
this.indentLevel++;
fn();
this.indentLevel--;
this.indent();
this.code += '}';
this.nl();
}

public exportDeclaration(exportType: ExportType, fn: () => void): void {
this.indent();
this.code += 'export ';
if (exportType == ExportType.default) {
this.code += 'default ';
}
fn();
}

public class(name: string, props: boolean, fn?: () => void): void {
this.code += `class ${name} extends React.Component<${props ? `${name}Props` : 'any'}, any> {`;
this.nl();
this.indentLevel++;
if (fn) {
fn();
}
this.indentLevel--;
this.indent();
this.code += '}';
this.nl();
}

public toString(): string {
return this.code;
}

}
Loading

0 comments on commit c7a988b

Please sign in to comment.