Skip to content
Permalink
e163ea39a8
Go to file
 
 
Cannot retrieve contributors at this time
500 lines (441 sloc) 14.9 KB
/* eslint-disable no-restricted-syntax, no-use-before-define, @typescript-eslint/no-use-before-define */
import ts from 'typescript';
import { Plugin } from 'ts-migrate-server';
import {
isReactClassComponent,
getReactComponentHeritageType,
isReactSfcFunctionDeclaration,
isReactSfcArrowFunction,
} from './utils/react';
import isNotNull from '../utils/isNotNull';
import updateSourceText, { SourceTextUpdate } from '../utils/updateSourceText';
import getTypeFromPropTypesObjectLiteral, { createPropsTypeNameGetter } from './utils/react-props';
import { getTextPreservingWhitespace } from './utils/text';
import { updateImports, DefaultImport, NamedImport } from './utils/imports';
type Options = {
anyAlias?: string;
anyFunctionAlias?: string;
shouldUpdateAirbnbImports?: boolean;
};
const reactPropsPlugin: Plugin<Options> = {
name: 'react-props',
run({ fileName, sourceFile, options }) {
if (!fileName.endsWith('.tsx')) return undefined;
const updates: SourceTextUpdate[] = [];
const getPropsTypeName = createPropsTypeNameGetter(sourceFile);
for (const node of sourceFile.statements) {
if (isReactNode(node)) {
const componentName = getComponentName(node);
const propsTypeName = getPropsTypeName(componentName);
updates.push(...updatePropTypes(node, propsTypeName, sourceFile, options));
}
}
const updatedSourceText = updateSourceText(sourceFile.text, updates);
const updatedSourceFile = ts.createSourceFile(
fileName,
updatedSourceText,
sourceFile.languageVersion,
);
const importUpdates = updateImports(
updatedSourceFile,
spreadReplacements.map((cur) => cur.typeImport),
[
{ moduleSpecifier: 'prop-types' },
...(options.shouldUpdateAirbnbImports ? importReplacements : []),
...(options.shouldUpdateAirbnbImports
? spreadReplacements.map((cur) => cur.spreadImport)
: []),
],
);
return updateSourceText(updatedSourceText, importUpdates);
},
};
export default reactPropsPlugin;
type SpreadReplacement = {
spreadId: string;
spreadImport: DefaultImport | NamedImport;
typeRef: ts.TypeReferenceNode;
typeImport: DefaultImport | NamedImport;
};
// airbnb related imports
const importReplacements = [{ moduleSpecifier: 'airbnb-prop-types' }];
const spreadReplacements: SpreadReplacement[] = [
{
spreadId: 'withStylesPropTypes',
spreadImport: {
namedImport: 'withStylesPropTypes',
moduleSpecifier: ':dls-themes/withStyles',
},
typeRef: ts.createTypeReferenceNode('WithStylesProps', undefined),
typeImport: {
namedImport: 'WithStylesProps',
moduleSpecifier: ':dls-themes/withStyles',
},
},
{
spreadId: 'withBreakpointPropTypes',
spreadImport: {
namedImport: 'withBreakpointPropTypes',
moduleSpecifier: ':dls-core/components/breakpoints/withBreakpoint',
},
typeRef: ts.createTypeReferenceNode('WithBreakpointProps', undefined),
typeImport: {
namedImport: 'WithBreakpointProps',
moduleSpecifier: ':dls-core/components/breakpoints/withBreakpoint',
},
},
{
spreadId: 'withRouterPropTypes',
spreadImport: {
defaultImport: 'withRouterPropTypes',
moduleSpecifier: ':routing/shapes/RR4PropTypes',
},
typeRef: ts.createTypeReferenceNode('RouteConfigComponentProps', [
ts.createTypeLiteralNode([]),
]),
typeImport: {
namedImport: 'RouteConfigComponentProps',
moduleSpecifier: 'react-router-config',
},
},
];
type ReactNode = ts.ClassDeclaration | ts.FunctionDeclaration | ts.VariableStatement;
type ReactSfcNode = ts.FunctionDeclaration | ts.VariableStatement;
function isReactNode(node: ts.Node): node is ReactNode {
return (
(ts.isClassDeclaration(node) && isReactClassComponent(node)) ||
(ts.isFunctionDeclaration(node) && isReactSfcFunctionDeclaration(node)) ||
(ts.isVariableStatement(node) && isReactSfcArrowFunction(node))
);
}
function isReactSfcNode(node: ReactNode): node is ReactSfcNode {
return ts.isFunctionDeclaration(node) || ts.isVariableStatement(node);
}
function updatePropTypes(
node: ReactNode,
propsTypeName: string,
sourceFile: ts.SourceFile,
options: Options,
) {
const updates: SourceTextUpdate[] = [];
const printer = ts.createPrinter();
if (isReactSfcNode(node)) {
const propsParam = getPropsParam(node);
if (propsParam && !propsParam.type) {
const propTypesNode = findSfcPropTypesNode(node, sourceFile);
const objectLiteral = propTypesNode && findPropTypesObjectLiteral(propTypesNode, sourceFile);
if (objectLiteral) {
updates.push(
...updateObjectLiteral(node, objectLiteral, propsTypeName, sourceFile, options, false),
);
updates.push({
kind: 'replace',
index: propsParam.pos,
length: propsParam.end - propsParam.pos,
text: printer.printNode(
ts.EmitHint.Unspecified,
ts.updateParameter(
propsParam,
propsParam.decorators,
propsParam.modifiers,
propsParam.dotDotDotToken,
propsParam.name,
propsParam.questionToken,
ts.createTypeReferenceNode(propsTypeName, undefined),
propsParam.initializer,
),
sourceFile,
),
});
updates.push(...deleteSfcPropTypes(node, sourceFile));
}
}
} else {
const heritageType = getReactComponentHeritageType(node)!;
const heritageTypeArgs = heritageType.typeArguments || [];
const propsType = heritageTypeArgs[0];
const stateType = heritageTypeArgs[1];
if (!propsType || isEmptyTypeLiteral(propsType)) {
const propTypesNode = findClassPropTypesNode(node, sourceFile);
const objectLiteral = propTypesNode && findPropTypesObjectLiteral(propTypesNode, sourceFile);
if (objectLiteral) {
updates.push(
...updateObjectLiteral(node, objectLiteral, propsTypeName, sourceFile, options, true),
);
updates.push({
kind: 'replace',
index: heritageType.pos,
length: heritageType.end - heritageType.pos,
text: ` ${printer.printNode(
ts.EmitHint.Unspecified,
ts.updateExpressionWithTypeArguments(
heritageType,
[ts.createTypeReferenceNode(propsTypeName, undefined), stateType].filter(
isNotNull,
) as any,
heritageType.expression,
),
sourceFile,
)}`,
});
updates.push(...deleteClassPropTypes(node, sourceFile));
}
}
}
return updates;
}
function isEmptyTypeLiteral(node: ts.Node) {
return ts.isTypeLiteralNode(node) && node.members.length === 0;
}
function updateObjectLiteral(
node: ReactNode,
objectLiteral: ts.ObjectLiteralExpression,
propsTypeName: string,
sourceFile: ts.SourceFile,
options: Options,
implicitChildren: boolean,
) {
const updates: SourceTextUpdate[] = [];
const printer = ts.createPrinter();
const propsTypeNode = getTypeFromPropTypesObjectLiteral(objectLiteral, sourceFile, {
anyAlias: options.anyAlias,
anyFunctionAlias: options.anyFunctionAlias,
implicitChildren,
spreadReplacements,
});
let propsTypeAlias = ts.createTypeAliasDeclaration(
undefined,
undefined,
propsTypeName,
undefined,
propsTypeNode,
);
propsTypeAlias = ts.moveSyntheticComments(propsTypeAlias, propsTypeNode);
const varStatement = getParentVariableStatement(objectLiteral, sourceFile);
if (varStatement) {
updates.push({
kind: 'replace',
index: varStatement.pos,
length: varStatement.end - varStatement.pos,
text: getTextPreservingWhitespace(varStatement, propsTypeAlias, sourceFile),
});
} else {
updates.push({
kind: 'insert',
index: node.pos,
text: `\n\n${printer.printNode(ts.EmitHint.Unspecified, propsTypeAlias, sourceFile)}`,
});
}
return updates;
}
function getComponentName(node: ReactNode) {
if (ts.isClassDeclaration(node) || ts.isFunctionDeclaration(node)) {
return node.name && node.name.text;
}
if (ts.isVariableStatement(node)) {
const declaration = node.declarationList.declarations[0];
return declaration && declaration.name && ts.isIdentifier(declaration.name)
? declaration.name.text
: undefined;
}
return undefined;
}
function getPropsParam(node: ReactSfcNode) {
if (ts.isFunctionDeclaration(node)) {
return node.parameters[0];
}
if (ts.isVariableStatement(node)) {
const declaration = node.declarationList.declarations[0];
const init = declaration && declaration.initializer;
const arrowFunction = init && ts.isArrowFunction(init) ? init : undefined;
return arrowFunction && arrowFunction.parameters[0];
}
return undefined;
}
function getParentVariableStatement(
objectLiteral: ts.ObjectLiteralExpression,
sourceFile: ts.SourceFile,
): ts.VariableStatement | undefined {
let cur: ts.Node = objectLiteral;
while (cur !== sourceFile) {
if (ts.isVariableStatement(cur)) {
return cur;
}
cur = cur.parent;
}
return undefined;
}
function deleteClassPropTypes(classDeclaration: ts.ClassDeclaration, sourceFile: ts.SourceFile) {
const updates: SourceTextUpdate[] = [];
for (const member of classDeclaration.members) {
if (isPropTypesStatic(member)) {
updates.push({
kind: 'delete',
index: member.pos,
length: member.end - member.pos,
});
if (member.initializer && ts.isIdentifier(member.initializer)) {
updates.push(...deleteIdRef(member.initializer, sourceFile));
}
}
}
const className = classDeclaration.name && classDeclaration.name.text;
if (className) {
updates.push(...deletePropTypesStatements(className, sourceFile));
}
return updates;
}
function deleteSfcPropTypes(node: ReactSfcNode, sourceFile: ts.SourceFile) {
const componentName = getComponentName(node);
return componentName ? deletePropTypesStatements(componentName, sourceFile) : [];
}
function deletePropTypesStatements(componentName: string, sourceFile: ts.SourceFile) {
const updates: SourceTextUpdate[] = [];
for (const statement of sourceFile.statements) {
if (isPropTypesStatement(statement, componentName)) {
updates.push({
kind: 'delete',
index: statement.pos,
length: statement.end - statement.pos,
});
if (
ts.isBinaryExpression(statement.expression) &&
ts.isIdentifier(statement.expression.right)
) {
updates.push(...deleteIdRef(statement.expression.right, sourceFile));
}
}
}
return updates;
}
function deleteIdRef(idenifier: ts.Identifier, sourceFile: ts.SourceFile) {
const updates: SourceTextUpdate[] = [];
for (const statement of sourceFile.statements) {
if (ts.isVariableDeclarationList(statement) && statement.declarations.length === 1) {
const declaration = statement.declarations[0];
if (
ts.isVariableDeclaration(declaration) &&
ts.isIdentifier(declaration.name) &&
declaration.name.text === idenifier.text
) {
if (declaration.initializer && ts.isIdentifier(declaration.initializer)) {
updates.push(
{
kind: 'delete',
index: declaration.pos,
length: declaration.end - declaration.pos,
},
...deleteIdRef(declaration.initializer, sourceFile),
);
}
}
}
}
return updates;
}
function isPropTypesStatic(member: ts.ClassElement): member is ts.PropertyDeclaration {
return (
ts.isPropertyDeclaration(member) &&
member.modifiers != null &&
member.modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword) &&
ts.isIdentifier(member.name) &&
member.name.text === 'propTypes' &&
member.initializer != null
);
}
function isPropTypesStatement(
statement: ts.Statement,
componentName: string,
): statement is ts.ExpressionStatement {
return (
ts.isExpressionStatement(statement) &&
ts.isBinaryExpression(statement.expression) &&
ts.isPropertyAccessExpression(statement.expression.left) &&
ts.isIdentifier(statement.expression.left.expression) &&
statement.expression.left.expression.text === componentName &&
ts.isIdentifier(statement.expression.left.name) &&
statement.expression.left.name.text === 'propTypes'
);
}
function findClassPropTypesNode(
classDeclaration: ts.ClassDeclaration,
sourceFile: ts.SourceFile,
): ts.PropertyDeclaration | ts.ExpressionStatement | undefined {
for (const member of classDeclaration.members) {
if (isPropTypesStatic(member)) {
return member;
}
}
const componentName = classDeclaration.name && classDeclaration.name.text;
for (const statement of sourceFile.statements) {
if (componentName && isPropTypesStatement(statement, componentName)) {
return statement;
}
}
return undefined;
}
function findSfcPropTypesNode(
node: ReactSfcNode,
sourceFile: ts.SourceFile,
): ts.PropertyDeclaration | ts.ExpressionStatement | undefined {
const componentName = getComponentName(node);
for (const statement of sourceFile.statements) {
if (componentName && isPropTypesStatement(statement, componentName)) {
return statement;
}
}
return undefined;
}
function findPropTypesObjectLiteral(
node: ts.PropertyDeclaration | ts.ExpressionStatement | undefined,
sourceFile: ts.SourceFile,
): ts.ObjectLiteralExpression | undefined {
if (!node) return undefined;
let expression: ts.Expression | undefined;
if (ts.isPropertyDeclaration(node) && node.initializer != null) {
expression = node.initializer;
} else if (ts.isExpressionStatement(node) && ts.isBinaryExpression(node.expression)) {
expression = node.expression.right;
}
return unpackInitializer(expression, sourceFile);
}
function unpackInitializer(
initializer: ts.Expression | undefined,
sourceFile: ts.SourceFile,
): ts.ObjectLiteralExpression | undefined {
if (!initializer) {
return undefined;
}
if (ts.isObjectLiteralExpression(initializer)) {
return initializer;
}
if (
ts.isCallExpression(initializer) &&
ts.isIdentifier(initializer.expression) &&
initializer.expression.text === 'forbidExtraProps' &&
initializer.arguments.length === 1
) {
const arg = initializer.arguments[0];
if (ts.isObjectLiteralExpression(arg)) {
return arg;
}
}
if (ts.isIdentifier(initializer)) {
for (const statement of sourceFile.statements) {
if (
ts.isVariableStatement(statement) &&
statement.declarationList.declarations.length === 1
) {
const declaration = statement.declarationList.declarations[0];
if (
ts.isVariableDeclaration(declaration) &&
ts.isIdentifier(declaration.name) &&
declaration.name.text === initializer.text
) {
return unpackInitializer(declaration.initializer, sourceFile);
}
}
}
}
return undefined;
}
You can’t perform that action at this time.