UNPKG

@xtrek/ts-migrate-plugins

Version:

Set of codemods, which are doing transformation of js/jsx to ts/tsx

220 lines 16 kB
"use strict"; 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