@xtrek/ts-migrate-plugins
Version:
Set of codemods, which are doing transformation of js/jsx to ts/tsx
220 lines • 16 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const typescript_1 = __importDefault(require("typescript"));
const updateSourceText_1 = __importDefault(require("../utils/updateSourceText"));
const validateOptions_1 = require("../utils/validateOptions");
const optionProperties = {
useDefaultPropsHelper: { type: 'boolean' },
};
/**
* At first, we are going to check is there any
* - `CompName.defaultProps = defaultPropsName;`
* - `static defaultProps = defaultPropsName`
* in the file
*/
const WITH_DEFAULT_PROPS_HELPER = `WithDefaultProps`;
const reactDefaultPropsPlugin = {
name: 'react-default-props',
run({ sourceFile, text, options }) {
const importDeclarations = sourceFile.statements.filter(typescript_1.default.isImportDeclaration);
const expressionStatements = sourceFile.statements.filter(typescript_1.default.isExpressionStatement);
const classDeclarations = sourceFile.statements.filter(typescript_1.default.isClassDeclaration);
const interfaceDeclarations = sourceFile.statements.filter(typescript_1.default.isInterfaceDeclaration);
// sfcs default props assignments
const sfcsDefaultPropsAssignments = expressionStatements.filter((expressionStatement) => typescript_1.default.isBinaryExpression(expressionStatement.expression) &&
typescript_1.default.isPropertyAccessExpression(expressionStatement.expression.left) &&
expressionStatement.expression.left.name.getText() === 'defaultProps');
// class default props assigments
const classDefaultPropsAssignment = classDeclarations.filter((classDeclaration) => classDeclaration.members
.filter(typescript_1.default.isPropertyDeclaration)
.filter((declaration) => declaration.name.getText() === 'defaultProps').length > 0);
if (sfcsDefaultPropsAssignments.length === 0 && classDefaultPropsAssignment.length === 0) {
return undefined;
}
const functionDeclarations = sourceFile.statements.filter(typescript_1.default.isFunctionDeclaration);
const variableStatements = sourceFile.statements.filter(typescript_1.default.isVariableStatement);
// will use Props type from here
const typeAliasDeclarations = sourceFile.statements.filter(typescript_1.default.isTypeAliasDeclaration);
const updates = [];
const printer = typescript_1.default.createPrinter();
const processedPropTypes = new Map();
let shouldAddWithDefaultPropsImport = !importDeclarations.some((importDeclaration) => /WithDefaultProps/.test(importDeclaration.moduleSpecifier.getText()));
const insertWithDefaultPropsImport = () => {
if (shouldAddWithDefaultPropsImport) {
updates.push({
kind: 'insert',
index: 0,
text: `${printer.printNode(typescript_1.default.EmitHint.Unspecified, getWithDefaultPropsImport(), sourceFile)}\n`,
});
// it probably could be done in the better way :)
shouldAddWithDefaultPropsImport = false;
}
};
const modifyAndInsertPropsType = (propsTypeAliasDeclaration, defaultPropsTypeName, propsTypeName, newTypeInsertPos, componentTypeReference, componentName) => {
// we don't want process props types more than once
if (processedPropTypes.get(propsTypeName) === defaultPropsTypeName)
return;
// prevent multiple usage of defalut props or WithDefaultProps
const alreadyHaveDefalutProps = typescript_1.default.isIntersectionTypeNode(propsTypeAliasDeclaration.type)
? propsTypeAliasDeclaration.type.types.some((typeExp) => (typescript_1.default.isTypeQueryNode(typeExp) &&
typeExp.exprName.getText() === defaultPropsTypeName) ||
typeExp.getText().includes(WITH_DEFAULT_PROPS_HELPER))
: typescript_1.default.isTypeReferenceNode(propsTypeAliasDeclaration.type) &&
propsTypeAliasDeclaration.type.typeName.getText() === WITH_DEFAULT_PROPS_HELPER;
if (alreadyHaveDefalutProps)
return;
if (options.useDefaultPropsHelper)
insertWithDefaultPropsImport();
const indexOfTypeValue = typescript_1.default.isIntersectionTypeNode(propsTypeAliasDeclaration.type)
? propsTypeAliasDeclaration.type.types.findIndex((typeEl) => typescript_1.default.isTypeLiteralNode(typeEl))
: -1;
const propTypesAreOnlyReferences = typescript_1.default.isIntersectionTypeNode(propsTypeAliasDeclaration.type) && indexOfTypeValue === -1;
const propsTypeValueNode = typescript_1.default.isIntersectionTypeNode(propsTypeAliasDeclaration.type)
? typescript_1.default.factory.updateIntersectionTypeNode(propsTypeAliasDeclaration.type, typescript_1.default.factory.createNodeArray(propsTypeAliasDeclaration.type.types.filter((_, k) => propTypesAreOnlyReferences || indexOfTypeValue === k)))
: propsTypeAliasDeclaration.type;
const doesPropsTypeHaveExport = propsTypeAliasDeclaration.modifiers &&
propsTypeAliasDeclaration.modifiers.find((modifier) => modifier.kind === typescript_1.default.SyntaxKind.ExportKeyword);
// rename type PropName -> type OwnPropName
let updatedProptypesName = `Own${propsTypeName}`;
// not an ideal way to prevent a double declaration of the OwnPropname,
// however, this should cover most of the cases
const alreadyHaveUpdatedName = interfaceDeclarations.some((node) => node.name.text.includes(updatedProptypesName)) ||
typeAliasDeclarations.some((node) => node.name.text.includes(updatedProptypesName));
updatedProptypesName = alreadyHaveUpdatedName
? `Own${componentName}${propsTypeName}`
: updatedProptypesName;
const updatedPropTypesName = doesPropsTypeHaveExport ? propsTypeName : updatedProptypesName;
const updatedPropTypeAlias = typescript_1.default.factory.updateTypeAliasDeclaration(propsTypeAliasDeclaration, propsTypeAliasDeclaration.decorators, propsTypeAliasDeclaration.modifiers, typescript_1.default.factory.createIdentifier(updatedPropTypesName), propsTypeAliasDeclaration.typeParameters, propsTypeValueNode);
const index = propsTypeAliasDeclaration.pos;
const length = propsTypeAliasDeclaration.end - index;
const text = printer.printNode(typescript_1.default.EmitHint.Unspecified, updatedPropTypeAlias, sourceFile);
updates.push({ kind: 'replace', index, length, text: `\n\n${text}` });
// create type Props = WithDefaultProps<OwnProps, typeof defaultProps> & types;
const newPropsTypeValue = options.useDefaultPropsHelper
? typescript_1.default.factory.createTypeReferenceNode(WITH_DEFAULT_PROPS_HELPER, [
typescript_1.default.factory.createTypeReferenceNode(updatedPropTypesName, undefined),
typescript_1.default.factory.createTypeQueryNode(typescript_1.default.factory.createIdentifier(defaultPropsTypeName)),
])
: typescript_1.default.factory.createIntersectionTypeNode([
typescript_1.default.factory.createTypeReferenceNode(updatedPropTypesName, undefined),
typescript_1.default.factory.createTypeQueryNode(typescript_1.default.factory.createIdentifier(defaultPropsTypeName)),
]);
const componentPropsTypeName = doesPropsTypeHaveExport
? `Private${propsTypeName}`
: propsTypeName;
const newPropsTypeAlias = typescript_1.default.factory.createTypeAliasDeclaration(undefined, undefined, typescript_1.default.factory.createIdentifier(componentPropsTypeName), undefined, typescript_1.default.isIntersectionTypeNode(propsTypeAliasDeclaration.type)
? typescript_1.default.factory.createIntersectionTypeNode([
newPropsTypeValue,
...propsTypeAliasDeclaration.type.types.filter((el, k) => propTypesAreOnlyReferences
? typescript_1.default.isIntersectionTypeNode(updatedPropTypeAlias) &&
!updatedPropTypeAlias.types.includes(el)
: indexOfTypeValue !== k),
])
: newPropsTypeValue);
updates.push({
kind: 'insert',
index: newTypeInsertPos,
text: `\n\n${printer.printNode(typescript_1.default.EmitHint.Unspecified, newPropsTypeAlias, sourceFile)}`,
});
// we should rename component prop type in that case
if (doesPropsTypeHaveExport) {
const updatedComponentTypeReference = typescript_1.default.factory.updateTypeReferenceNode(componentTypeReference, typescript_1.default.factory.createIdentifier(componentPropsTypeName), undefined);
const index = componentTypeReference.pos;
const length = componentTypeReference.end - index;
const text = printer.printNode(typescript_1.default.EmitHint.Unspecified, updatedComponentTypeReference, sourceFile);
updates.push({ kind: 'replace', index, length, text: `${text}` });
}
processedPropTypes.set(propsTypeName, defaultPropsTypeName);
};
/* process all default props assignments:
* - find out, is it object literal expression or identifier obj reference
* - if it's obj literal - use `CompName.defaultProps`, otherwise - save variable name
* - create OwnProp type
* - create/modify type Props = WithDefaultProps
* - use a new Props type in the component
*/
sfcsDefaultPropsAssignments.forEach((defaultProp) => {
const expression = defaultProp.expression;
const leftPart = expression.left;
const componentName = leftPart.expression.getText();
const isObjectIdentifier = typescript_1.default.isIdentifier(expression.right);
const defaultPropsTypeName = isObjectIdentifier
? expression.right.getText()
: `${componentName}.defaultProps`;
const variableDeclaration = variableStatements.find((variableStatement) => !!variableStatement.declarationList.declarations.find((declaration) => !!(declaration.name && declaration.name.getText() === componentName)));
const variableInitializer = variableDeclaration
? variableDeclaration.declarationList.declarations[0].initializer
: undefined;
const componentDeclaration = functionDeclarations.find((functionDeclaration) => !!(functionDeclaration.name && functionDeclaration.name.getText() === componentName)) ||
(variableInitializer &&
(typescript_1.default.isArrowFunction(variableInitializer) || typescript_1.default.isFunctionDeclaration(variableInitializer))
? variableInitializer
: undefined);
// hasPropsTypeReference
if (componentDeclaration &&
componentDeclaration.parameters.length === 1 &&
componentDeclaration.parameters[0].type !== undefined &&
typescript_1.default.isTypeReferenceNode(componentDeclaration.parameters[0].type)) {
const propsTypeName = componentDeclaration.parameters[0].type.getText();
const propsTypeAliasDeclarations = typeAliasDeclarations.find((typeAlias) => typeAlias.name.getText() === propsTypeName);
if (propsTypeAliasDeclarations) {
const newTypeInsertPos = variableDeclaration && variableInitializer
? variableDeclaration.pos
: componentDeclaration.pos;
modifyAndInsertPropsType(propsTypeAliasDeclarations, defaultPropsTypeName, propsTypeName, newTypeInsertPos, componentDeclaration.parameters[0].type, componentName);
}
}
});
classDefaultPropsAssignment.forEach((classDeclaration) => {
const componentName = classDeclaration.name && classDeclaration.name.getText();
const defaultPropsDeclaration = classDeclaration.members
.filter(typescript_1.default.isPropertyDeclaration)
.filter((declaration) => declaration.name.getText() === 'defaultProps')[0];
if (!defaultPropsDeclaration)
return;
const isObjectIdentifier = defaultPropsDeclaration.initializer && typescript_1.default.isIdentifier(defaultPropsDeclaration.initializer);
const defaultPropsTypeName = isObjectIdentifier && defaultPropsDeclaration.initializer
? defaultPropsDeclaration.initializer.getText()
: `${componentName}.defaultProps`;
const variableDeclaration = variableStatements.find((variableStatement) => !!variableStatement.declarationList.declarations.find((declaration) => !!(declaration.name && declaration.name.getText() === componentName)));
const variableInitializer = variableDeclaration
? variableDeclaration.declarationList.declarations[0].initializer
: undefined;
const heritageClause = classDeclaration.heritageClauses
? classDeclaration.heritageClauses.find((heritageClause) => heritageClause.types.length > 0)
: undefined;
const expressionWithTypeArguments = heritageClause && heritageClause.types
? heritageClause.types.find((x) => typescript_1.default.isExpressionWithTypeArguments(x) &&
(x.typeArguments ? x.typeArguments.length > 0 : false))
: undefined;
const propsTypeReferenceNode = expressionWithTypeArguments && expressionWithTypeArguments.typeArguments
? expressionWithTypeArguments.typeArguments.find((genericArgs) => typescript_1.default.isTypeReferenceNode(genericArgs))
: undefined;
// wasn't able to find a typename of the props :(
if (!propsTypeReferenceNode || !componentName)
return;
const propsTypeName = propsTypeReferenceNode.getText();
const propsTypeAliasDeclarations = typeAliasDeclarations.find((typeAlias) => typeAlias.name.getText() === propsTypeName);
if (propsTypeAliasDeclarations) {
const newTypeInsertPos = variableDeclaration && variableInitializer
? variableDeclaration.pos
: classDeclaration.pos;
modifyAndInsertPropsType(propsTypeAliasDeclarations, defaultPropsTypeName, propsTypeName, newTypeInsertPos, propsTypeReferenceNode, componentName);
}
});
return updateSourceText_1.default(text, updates);
},
validate: validateOptions_1.createValidate(optionProperties),
};
// the target project might not have this as an internal dependency in project.json
// It would have to be manually added, otherwise CI will complain about it
function getWithDefaultPropsImport() {
return typescript_1.default.factory.createImportDeclaration(undefined, undefined, typescript_1.default.factory.createImportClause(false, undefined, typescript_1.default.factory.createNamedImports([
typescript_1.default.factory.createImportSpecifier(undefined, typescript_1.default.factory.createIdentifier('WithDefaultProps')),
])), typescript_1.default.factory.createStringLiteral(':ts-utils/types/WithDefaultProps'));
}
exports.default = reactDefaultPropsPlugin;
//# sourceMappingURL=react-default-props.js.map