Skip to content

Commit f84e4e8

Browse files
vincentbelmohsen1
authored andcommitted
Support stateless component (lyft#23)
* remove hoist transform * support stateless component * don't make type alias if prop or state types are empty * fix: should only transform current source file
1 parent 623cffe commit f84e4e8

File tree

37 files changed

+755
-531
lines changed

37 files changed

+755
-531
lines changed

Diff for: src/compiler.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ export function compile(filePath: string, factoryFactories: TransformFactoryFact
1414
};
1515

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

2023
const result = ts.transform(

Diff for: src/helpers/build-prop-type-interface.ts

+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import * as ts from 'typescript';
2+
3+
/**
4+
* Build props interface from propTypes object
5+
* @example
6+
* {
7+
* foo: React.PropTypes.string.isRequired
8+
* }
9+
*
10+
* becomes
11+
* {
12+
* foo: string;
13+
* }
14+
* @param objectLiteral
15+
*/
16+
export function buildInterfaceFromPropTypeObjectLiteral(objectLiteral: ts.ObjectLiteralExpression) {
17+
const members = objectLiteral.properties
18+
// We only need to process PropertyAssignment:
19+
// {
20+
// a: 123 // PropertyAssignment
21+
// }
22+
//
23+
// filter out:
24+
// {
25+
// a() {}, // MethodDeclaration
26+
// b, // ShorthandPropertyAssignment
27+
// ...c, // SpreadAssignment
28+
// get d() {}, // AccessorDeclaration
29+
// }
30+
.filter(ts.isPropertyAssignment)
31+
// Ignore children, React types have it
32+
.filter(property => property.name.getText() !== 'children')
33+
.map(propertyAssignment => {
34+
const name = propertyAssignment.name.getText();
35+
const initializer = propertyAssignment.initializer;
36+
const isRequired = isPropTypeRequired(initializer);
37+
const typeExpression = isRequired
38+
? // We have guaranteed the type in `isPropTypeRequired()`
39+
(initializer as ts.PropertyAccessExpression).expression
40+
: initializer;
41+
const typeValue = getTypeFromReactPropTypeExpression(typeExpression);
42+
43+
return ts.createPropertySignature(
44+
[],
45+
name,
46+
isRequired ? undefined : ts.createToken(ts.SyntaxKind.QuestionToken),
47+
typeValue,
48+
undefined,
49+
);
50+
});
51+
52+
return ts.createTypeLiteralNode(members);
53+
}
54+
55+
/**
56+
* Turns React.PropTypes.* into TypeScript type value
57+
*
58+
* @param node React propTypes value
59+
*/
60+
function getTypeFromReactPropTypeExpression(node: ts.Expression): ts.TypeNode {
61+
let result = null;
62+
if (ts.isPropertyAccessExpression(node)) {
63+
/**
64+
* PropTypes.array,
65+
* PropTypes.bool,
66+
* PropTypes.func,
67+
* PropTypes.number,
68+
* PropTypes.object,
69+
* PropTypes.string,
70+
* PropTypes.symbol, (ignore)
71+
* PropTypes.node,
72+
* PropTypes.element,
73+
* PropTypes.any,
74+
*/
75+
const text = node.getText().replace(/React\.PropTypes\./, '');
76+
77+
if (/string/.test(text)) {
78+
result = ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
79+
} else if (/any/.test(text)) {
80+
result = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
81+
} else if (/array/.test(text)) {
82+
result = ts.createArrayTypeNode(ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
83+
} else if (/bool/.test(text)) {
84+
result = ts.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
85+
} else if (/number/.test(text)) {
86+
result = ts.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
87+
} else if (/object/.test(text)) {
88+
result = ts.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword);
89+
} else if (/node/.test(text)) {
90+
result = ts.createTypeReferenceNode('React.ReactNode', []);
91+
} else if (/element/.test(text)) {
92+
result = ts.createTypeReferenceNode('JSX.Element', []);
93+
} else if (/func/.test(text)) {
94+
const arrayOfAny = ts.createParameter(
95+
[],
96+
[],
97+
ts.createToken(ts.SyntaxKind.DotDotDotToken),
98+
'args',
99+
undefined,
100+
ts.createArrayTypeNode(ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)),
101+
undefined,
102+
);
103+
result = ts.createFunctionTypeNode([], [arrayOfAny], ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword));
104+
}
105+
} else if (ts.isCallExpression(node)) {
106+
/**
107+
* PropTypes.instanceOf(), (ignore)
108+
* PropTypes.oneOf(), // only support oneOf([1, 2]), oneOf(['a', 'b'])
109+
* PropTypes.oneOfType(),
110+
* PropTypes.arrayOf(),
111+
* PropTypes.objectOf(),
112+
* PropTypes.shape(),
113+
*/
114+
const text = node.expression.getText();
115+
if (/oneOf$/.test(text)) {
116+
const argument = node.arguments[0];
117+
if (ts.isArrayLiteralExpression(argument)) {
118+
if (argument.elements.every(elm => ts.isStringLiteral(elm) || ts.isNumericLiteral(elm))) {
119+
result = ts.createUnionTypeNode(
120+
(argument.elements as ts.NodeArray<ts.StringLiteral | ts.NumericLiteral>).map(elm =>
121+
ts.createLiteralTypeNode(elm),
122+
),
123+
);
124+
}
125+
}
126+
} else if (/oneOfType$/.test(text)) {
127+
const argument = node.arguments[0];
128+
if (ts.isArrayLiteralExpression(argument)) {
129+
result = ts.createUnionOrIntersectionTypeNode(
130+
ts.SyntaxKind.UnionType,
131+
argument.elements.map(elm => getTypeFromReactPropTypeExpression(elm)),
132+
);
133+
}
134+
} else if (/arrayOf$/.test(text)) {
135+
const argument = node.arguments[0];
136+
if (argument) {
137+
result = ts.createArrayTypeNode(getTypeFromReactPropTypeExpression(argument));
138+
}
139+
} else if (/objectOf$/.test(text)) {
140+
const argument = node.arguments[0];
141+
if (argument) {
142+
result = ts.createTypeLiteralNode([
143+
ts.createIndexSignature(
144+
undefined,
145+
undefined,
146+
[
147+
ts.createParameter(
148+
undefined,
149+
undefined,
150+
undefined,
151+
'key',
152+
undefined,
153+
ts.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
154+
),
155+
],
156+
getTypeFromReactPropTypeExpression(argument),
157+
),
158+
]);
159+
}
160+
} else if (/shape$/.test(text)) {
161+
const argument = node.arguments[0];
162+
if (ts.isObjectLiteralExpression(argument)) {
163+
return buildInterfaceFromPropTypeObjectLiteral(argument);
164+
}
165+
}
166+
}
167+
168+
/**
169+
* customProp,
170+
* anything others
171+
*/
172+
if (result === null) {
173+
result = ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword);
174+
}
175+
176+
return result;
177+
}
178+
179+
/**
180+
* Decide if node is required
181+
* @param node React propTypes member node
182+
*/
183+
function isPropTypeRequired(node: ts.Expression) {
184+
if (!ts.isPropertyAccessExpression(node)) return false;
185+
186+
const text = node.getText().replace(/React\.PropTypes\./, '');
187+
return /\.isRequired/.test(text);
188+
}

Diff for: src/helpers/index.ts

+64-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as ts from 'typescript';
22
import * as _ from 'lodash';
33

4+
export * from './build-prop-type-interface';
5+
46
/**
57
* If a class declaration a react class?
68
* @param classDeclaration
@@ -50,10 +52,12 @@ export function isReactComponent(classDeclaration: ts.ClassDeclaration, typeChec
5052
* @param clause
5153
*/
5254
export function isReactHeritageClause(clause: ts.HeritageClause) {
53-
return clause.token === ts.SyntaxKind.ExtendsKeyword &&
55+
return (
56+
clause.token === ts.SyntaxKind.ExtendsKeyword &&
5457
clause.types.length === 1 &&
5558
ts.isExpressionWithTypeArguments(clause.types[0]) &&
56-
/Component/.test(clause.types[0].expression.getText());
59+
/Component/.test(clause.types[0].expression.getText())
60+
);
5761
}
5862

5963
/**
@@ -63,11 +67,13 @@ export function isReactHeritageClause(clause: ts.HeritageClause) {
6367
* @param statement
6468
*/
6569
export function isReactPropTypeAssignmentStatement(statement: ts.Statement): statement is ts.ExpressionStatement {
66-
return ts.isExpressionStatement(statement)
67-
&& ts.isBinaryExpression(statement.expression)
68-
&& statement.expression.operatorToken.kind === ts.SyntaxKind.FirstAssignment
69-
&& ts.isPropertyAccessExpression(statement.expression.left)
70-
&& /\.propTypes$|\.propTypes\..+$/.test(statement.expression.left.getText())
70+
return (
71+
ts.isExpressionStatement(statement) &&
72+
ts.isBinaryExpression(statement.expression) &&
73+
statement.expression.operatorToken.kind === ts.SyntaxKind.FirstAssignment &&
74+
ts.isPropertyAccessExpression(statement.expression.left) &&
75+
/\.propTypes$|\.propTypes\..+$/.test(statement.expression.left.getText())
76+
);
7177
}
7278

7379
/**
@@ -78,7 +84,7 @@ export function hasStaticModifier(classMember: ts.ClassElement) {
7884
if (!classMember.modifiers) {
7985
return false;
8086
}
81-
const staticModifier = _.find(classMember.modifiers, (modifier) => {
87+
const staticModifier = _.find(classMember.modifiers, modifier => {
8288
return modifier.kind == ts.SyntaxKind.StaticKeyword;
8389
});
8490
return staticModifier !== undefined;
@@ -91,12 +97,61 @@ export function hasStaticModifier(classMember: ts.ClassElement) {
9197
*/
9298
export function isPropTypesMember(classMember: ts.ClassElement, sourceFile: ts.SourceFile) {
9399
try {
94-
return classMember.name !== undefined && classMember.name.getFullText(sourceFile) !== 'propTypes'
100+
return classMember.name !== undefined && classMember.name.getFullText(sourceFile) !== 'propTypes';
95101
} catch (e) {
96102
return false;
97103
}
98104
}
99105

106+
/**
107+
* Get component name off of a propType assignment statement
108+
* @param propTypeAssignment
109+
* @param sourceFile
110+
*/
111+
export function getComponentName(propTypeAssignment: ts.Statement, sourceFile: ts.SourceFile) {
112+
const text = propTypeAssignment.getText(sourceFile);
113+
return text.substr(0, text.indexOf('.'));
114+
}
115+
116+
/**
117+
* Convert react stateless function to arrow function
118+
* @example
119+
* Before:
120+
* function Hello(message) {
121+
* return <div>{message}</div>
122+
* }
123+
*
124+
* After:
125+
* const Hello = message => {
126+
* return <div>{message}</div>
127+
* }
128+
*/
129+
export function convertReactStatelessFunctionToArrowFunction(
130+
statelessFunc: ts.FunctionDeclaration | ts.VariableStatement,
131+
) {
132+
if (ts.isVariableStatement(statelessFunc)) return statelessFunc;
133+
134+
const funcName = statelessFunc.name || 'Component';
135+
const funcBody = statelessFunc.body || ts.createBlock([]);
136+
137+
const initializer = ts.createArrowFunction(
138+
undefined,
139+
undefined,
140+
statelessFunc.parameters,
141+
undefined,
142+
undefined,
143+
funcBody,
144+
);
145+
146+
return ts.createVariableStatement(
147+
statelessFunc.modifiers,
148+
ts.createVariableDeclarationList(
149+
[ts.createVariableDeclaration(funcName, undefined, initializer)],
150+
ts.NodeFlags.Const,
151+
),
152+
);
153+
}
154+
100155
/**
101156
* Insert an item in middle of an array after a specific item
102157
* @param collection

Diff for: src/index.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import * as ts from 'typescript';
22

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

1111
export {
1212
reactMovePropTypesToClassTransformFactoryFactory,
1313
reactJSMakePropsAndStateInterfaceTransformFactoryFactory,
14-
reactHoistGenericsTransformFactoryFactory,
14+
reactStatelessFunctionMakePropsTransformFactoryFactory,
1515
collapseIntersectionInterfacesTransformFactoryFactory,
1616
reactRemovePropTypesAssignmentTransformFactoryFactory,
1717
reactRemoveStaticPropTypesMemberTransformFactoryFactory,
@@ -21,7 +21,7 @@ export {
2121
export const allTransforms = [
2222
reactMovePropTypesToClassTransformFactoryFactory,
2323
reactJSMakePropsAndStateInterfaceTransformFactoryFactory,
24-
reactHoistGenericsTransformFactoryFactory,
24+
reactStatelessFunctionMakePropsTransformFactoryFactory,
2525
collapseIntersectionInterfacesTransformFactoryFactory,
2626
reactRemovePropTypesAssignmentTransformFactoryFactory,
2727
reactRemoveStaticPropTypesMemberTransformFactoryFactory,

0 commit comments

Comments
 (0)