This repository has been archived by the owner on Nov 27, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
594e175
commit c7a988b
Showing
11 changed files
with
499 additions
and
390 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
Oops, something went wrong.