Skip to content
This repository was archived by the owner on Sep 21, 2019. It is now read-only.

Support stateless component #23

Merged
merged 4 commits into from
Jan 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export function compile(filePath: string, factoryFactories: TransformFactoryFact
};

const program = ts.createProgram([filePath], compilerOptions);
const sourceFiles = program.getSourceFiles().filter(sf => !sf.isDeclarationFile);
// `program.getSourceFiles()` will include those imported files,
// like: `import * as a from './file-a'`.
// We should only transform current file.
const sourceFiles = program.getSourceFiles().filter(sf => sf.fileName === filePath);
const typeChecker = program.getTypeChecker();

const result = ts.transform(
Expand Down
188 changes: 188 additions & 0 deletions src/helpers/build-prop-type-interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import * as ts from 'typescript';

/**
* Build props interface from propTypes object
* @example
* {
* foo: React.PropTypes.string.isRequired
* }
*
* becomes
* {
* foo: string;
* }
* @param objectLiteral
*/
export function buildInterfaceFromPropTypeObjectLiteral(objectLiteral: ts.ObjectLiteralExpression) {
const members = objectLiteral.properties
// We only need to process PropertyAssignment:
// {
// a: 123 // PropertyAssignment
// }
//
// filter out:
// {
// a() {}, // MethodDeclaration
// b, // ShorthandPropertyAssignment
// ...c, // SpreadAssignment
// get d() {}, // AccessorDeclaration
// }
.filter(ts.isPropertyAssignment)
// Ignore children, React types have it
.filter(property => property.name.getText() !== 'children')
.map(propertyAssignment => {
const name = propertyAssignment.name.getText();
const initializer = propertyAssignment.initializer;
const isRequired = isPropTypeRequired(initializer);
const typeExpression = isRequired
? // We have guaranteed the type in `isPropTypeRequired()`
(initializer as ts.PropertyAccessExpression).expression
: initializer;
const typeValue = getTypeFromReactPropTypeExpression(typeExpression);

return ts.createPropertySignature(
[],
name,
isRequired ? undefined : ts.createToken(ts.SyntaxKind.QuestionToken),
typeValue,
undefined,
);
});

return ts.createTypeLiteralNode(members);
}

/**
* Turns React.PropTypes.* into TypeScript type value
*
* @param node React propTypes value
*/
function getTypeFromReactPropTypeExpression(node: ts.Expression): ts.TypeNode {
let result = null;
if (ts.isPropertyAccessExpression(node)) {
/**
* PropTypes.array,
* PropTypes.bool,
* PropTypes.func,
* PropTypes.number,
* PropTypes.object,
* PropTypes.string,
* PropTypes.symbol, (ignore)
* PropTypes.node,
* PropTypes.element,
* PropTypes.any,
*/
const text = node.getText().replace(/React\.PropTypes\./, '');

if (/string/.test(text)) {
result = ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
} else if (/any/.test(text)) {
result = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
} else if (/array/.test(text)) {
result = ts.createArrayTypeNode(ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
} else if (/bool/.test(text)) {
result = ts.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
} else if (/number/.test(text)) {
result = ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
} else if (/object/.test(text)) {
result = ts.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword);
} else if (/node/.test(text)) {
result = ts.createTypeReferenceNode('React.ReactNode', []);
} else if (/element/.test(text)) {
result = ts.createTypeReferenceNode('JSX.Element', []);
} else if (/func/.test(text)) {
const arrayOfAny = ts.createParameter(
[],
[],
ts.createToken(ts.SyntaxKind.DotDotDotToken),
'args',
undefined,
ts.createArrayTypeNode(ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)),
undefined,
);
result = ts.createFunctionTypeNode([], [arrayOfAny], ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
}
} else if (ts.isCallExpression(node)) {
/**
* PropTypes.instanceOf(), (ignore)
* PropTypes.oneOf(), // only support oneOf([1, 2]), oneOf(['a', 'b'])
* PropTypes.oneOfType(),
* PropTypes.arrayOf(),
* PropTypes.objectOf(),
* PropTypes.shape(),
*/
const text = node.expression.getText();
if (/oneOf$/.test(text)) {
const argument = node.arguments[0];
if (ts.isArrayLiteralExpression(argument)) {
if (argument.elements.every(elm => ts.isStringLiteral(elm) || ts.isNumericLiteral(elm))) {
result = ts.createUnionTypeNode(
(argument.elements as ts.NodeArray<ts.StringLiteral | ts.NumericLiteral>).map(elm =>
ts.createLiteralTypeNode(elm),
),
);
}
}
} else if (/oneOfType$/.test(text)) {
const argument = node.arguments[0];
if (ts.isArrayLiteralExpression(argument)) {
result = ts.createUnionOrIntersectionTypeNode(
ts.SyntaxKind.UnionType,
argument.elements.map(elm => getTypeFromReactPropTypeExpression(elm)),
);
}
} else if (/arrayOf$/.test(text)) {
const argument = node.arguments[0];
if (argument) {
result = ts.createArrayTypeNode(getTypeFromReactPropTypeExpression(argument));
}
} else if (/objectOf$/.test(text)) {
const argument = node.arguments[0];
if (argument) {
result = ts.createTypeLiteralNode([
ts.createIndexSignature(
undefined,
undefined,
[
ts.createParameter(
undefined,
undefined,
undefined,
'key',
undefined,
ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
),
],
getTypeFromReactPropTypeExpression(argument),
),
]);
}
} else if (/shape$/.test(text)) {
const argument = node.arguments[0];
if (ts.isObjectLiteralExpression(argument)) {
return buildInterfaceFromPropTypeObjectLiteral(argument);
}
}
}

/**
* customProp,
* anything others
*/
if (result === null) {
result = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
}

return result;
}

/**
* Decide if node is required
* @param node React propTypes member node
*/
function isPropTypeRequired(node: ts.Expression) {
if (!ts.isPropertyAccessExpression(node)) return false;

const text = node.getText().replace(/React\.PropTypes\./, '');
return /\.isRequired/.test(text);
}
73 changes: 64 additions & 9 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as ts from 'typescript';
import * as _ from 'lodash';

export * from './build-prop-type-interface';

/**
* If a class declaration a react class?
* @param classDeclaration
Expand Down Expand Up @@ -50,10 +52,12 @@ export function isReactComponent(classDeclaration: ts.ClassDeclaration, typeChec
* @param clause
*/
export function isReactHeritageClause(clause: ts.HeritageClause) {
return clause.token === ts.SyntaxKind.ExtendsKeyword &&
return (
clause.token === ts.SyntaxKind.ExtendsKeyword &&
clause.types.length === 1 &&
ts.isExpressionWithTypeArguments(clause.types[0]) &&
/Component/.test(clause.types[0].expression.getText());
/Component/.test(clause.types[0].expression.getText())
);
}

/**
Expand All @@ -63,11 +67,13 @@ export function isReactHeritageClause(clause: ts.HeritageClause) {
* @param statement
*/
export function isReactPropTypeAssignmentStatement(statement: ts.Statement): statement is ts.ExpressionStatement {
return ts.isExpressionStatement(statement)
&& ts.isBinaryExpression(statement.expression)
&& statement.expression.operatorToken.kind === ts.SyntaxKind.FirstAssignment
&& ts.isPropertyAccessExpression(statement.expression.left)
&& /\.propTypes$|\.propTypes\..+$/.test(statement.expression.left.getText())
return (
ts.isExpressionStatement(statement) &&
ts.isBinaryExpression(statement.expression) &&
statement.expression.operatorToken.kind === ts.SyntaxKind.FirstAssignment &&
ts.isPropertyAccessExpression(statement.expression.left) &&
/\.propTypes$|\.propTypes\..+$/.test(statement.expression.left.getText())
);
}

/**
Expand All @@ -78,7 +84,7 @@ export function hasStaticModifier(classMember: ts.ClassElement) {
if (!classMember.modifiers) {
return false;
}
const staticModifier = _.find(classMember.modifiers, (modifier) => {
const staticModifier = _.find(classMember.modifiers, modifier => {
return modifier.kind == ts.SyntaxKind.StaticKeyword;
});
return staticModifier !== undefined;
Expand All @@ -91,12 +97,61 @@ export function hasStaticModifier(classMember: ts.ClassElement) {
*/
export function isPropTypesMember(classMember: ts.ClassElement, sourceFile: ts.SourceFile) {
try {
return classMember.name !== undefined && classMember.name.getFullText(sourceFile) !== 'propTypes'
return classMember.name !== undefined && classMember.name.getFullText(sourceFile) !== 'propTypes';
} catch (e) {
return false;
}
}

/**
* Get component name off of a propType assignment statement
* @param propTypeAssignment
* @param sourceFile
*/
export function getComponentName(propTypeAssignment: ts.Statement, sourceFile: ts.SourceFile) {
const text = propTypeAssignment.getText(sourceFile);
return text.substr(0, text.indexOf('.'));
}

/**
* Convert react stateless function to arrow function
* @example
* Before:
* function Hello(message) {
* return <div>{message}</div>
* }
*
* After:
* const Hello = message => {
* return <div>{message}</div>
* }
*/
export function convertReactStatelessFunctionToArrowFunction(
statelessFunc: ts.FunctionDeclaration | ts.VariableStatement,
) {
if (ts.isVariableStatement(statelessFunc)) return statelessFunc;

const funcName = statelessFunc.name || 'Component';
const funcBody = statelessFunc.body || ts.createBlock([]);

const initializer = ts.createArrowFunction(
undefined,
undefined,
statelessFunc.parameters,
undefined,
undefined,
funcBody,
);

return ts.createVariableStatement(
statelessFunc.modifiers,
ts.createVariableDeclarationList(
[ts.createVariableDeclaration(funcName, undefined, initializer)],
ts.NodeFlags.Const,
),
);
}

/**
* Insert an item in middle of an array after a specific item
* @param collection
Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import * as ts from 'typescript';

import { compile } from './compiler';
import { reactJSMakePropsAndStateInterfaceTransformFactoryFactory } from './transforms/react-js-make-props-and-state-transform';
import { reactHoistGenericsTransformFactoryFactory } from './transforms/react-hoist-generics-transform';
import { reactRemovePropTypesAssignmentTransformFactoryFactory } from './transforms/react-remove-prop-types-assignment-transform';
import { reactMovePropTypesToClassTransformFactoryFactory } from './transforms/react-move-prop-types-to-class-transform';
import { collapseIntersectionInterfacesTransformFactoryFactory } from './transforms/collapse-intersection-interfaces-transform';
import { reactRemoveStaticPropTypesMemberTransformFactoryFactory } from './transforms/react-remove-static-prop-types-member-transform';
import { reactStatelessFunctionMakePropsTransformFactoryFactory } from './transforms/react-stateless-function-make-props-transform';

export {
reactMovePropTypesToClassTransformFactoryFactory,
reactJSMakePropsAndStateInterfaceTransformFactoryFactory,
reactHoistGenericsTransformFactoryFactory,
reactStatelessFunctionMakePropsTransformFactoryFactory,
collapseIntersectionInterfacesTransformFactoryFactory,
reactRemovePropTypesAssignmentTransformFactoryFactory,
reactRemoveStaticPropTypesMemberTransformFactoryFactory,
Expand All @@ -21,7 +21,7 @@ export {
export const allTransforms = [
reactMovePropTypesToClassTransformFactoryFactory,
reactJSMakePropsAndStateInterfaceTransformFactoryFactory,
reactHoistGenericsTransformFactoryFactory,
reactStatelessFunctionMakePropsTransformFactoryFactory,
collapseIntersectionInterfacesTransformFactoryFactory,
reactRemovePropTypesAssignmentTransformFactoryFactory,
reactRemoveStaticPropTypesMemberTransformFactoryFactory,
Expand Down
Loading