Join GitHub today
GitHub is home to over 50 million developers working together to host and review code, manage projects, and build software together.
Sign upGitHub is where the world builds software
Millions of developers and companies build, ship, and maintain their software on GitHub — the largest and most advanced development platform in the world.
| import ts from 'typescript'; | |
| import { Plugin } from 'ts-migrate-server'; | |
| import updateSourceText, { SourceTextUpdate } from '../utils/updateSourceText'; | |
| type Options = { | |
| useDefaultPropsHelper?: 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: Plugin<Options> = { | |
| name: 'react-default-props', | |
| run({ sourceFile, text, options }) { | |
| const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration); | |
| const expressionStatements = sourceFile.statements.filter(ts.isExpressionStatement); | |
| const classDeclarations = sourceFile.statements.filter(ts.isClassDeclaration); | |
| // sfcs default props assignments | |
| const sfcsDefaultPropsAssignments = expressionStatements.filter( | |
| (expressionStatement) => | |
| ts.isBinaryExpression(expressionStatement.expression) && | |
| ts.isPropertyAccessExpression(expressionStatement.expression.left) && | |
| expressionStatement.expression.left.name.getText() === 'defaultProps', | |
| ); | |
| // class default props assigments | |
| const classDefaultPropsAssignment = classDeclarations.filter( | |
| (classDeclaration) => | |
| classDeclaration.members | |
| .filter(ts.isPropertyDeclaration) | |
| .filter((declaration) => declaration.name.getText() === 'defaultProps').length > 0, | |
| ); | |
| if (sfcsDefaultPropsAssignments.length === 0 && classDefaultPropsAssignment.length === 0) { | |
| return undefined; | |
| } | |
| const functionDeclarations = sourceFile.statements.filter(ts.isFunctionDeclaration); | |
| const variableStatements = sourceFile.statements.filter(ts.isVariableStatement); | |
| // will use Props type from here | |
| const typeAliasDeclarations = sourceFile.statements.filter(ts.isTypeAliasDeclaration); | |
| const updates: SourceTextUpdate[] = []; | |
| const printer = ts.createPrinter(); | |
| const processedPropTypes = new Map<string, string>(); | |
| let shouldAddWithDefaultPropsImport = !importDeclarations.some((importDeclaration) => | |
| /WithDefaultProps/.test(importDeclaration.moduleSpecifier.getText()), | |
| ); | |
| const insertWithDefaultPropsImport = () => { | |
| if (shouldAddWithDefaultPropsImport) { | |
| updates.push({ | |
| kind: 'insert', | |
| index: 0, | |
| text: `${printer.printNode( | |
| ts.EmitHint.Unspecified, | |
| getWithDefaultPropsImport(), | |
| sourceFile, | |
| )}\n`, | |
| }); | |
| // it probably could be done in the better way :) | |
| shouldAddWithDefaultPropsImport = false; | |
| } | |
| }; | |
| const modifyAndInsertPropsType = ( | |
| propsTypeAliasDeclaration: ts.TypeAliasDeclaration, | |
| defaultPropsTypeName: string, | |
| propsTypeName: string, | |
| newTypeInsertPos: number, | |
| componentTypeReference: ts.TypeReferenceNode, | |
| ) => { | |
| // 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 = ts.isIntersectionTypeNode(propsTypeAliasDeclaration.type) | |
| ? propsTypeAliasDeclaration.type.types.some( | |
| (typeExp) => | |
| (ts.isTypeQueryNode(typeExp) && | |
| typeExp.exprName.getText() === defaultPropsTypeName) || | |
| typeExp.getText().includes(WITH_DEFAULT_PROPS_HELPER), | |
| ) | |
| : ts.isTypeReferenceNode(propsTypeAliasDeclaration.type) && | |
| propsTypeAliasDeclaration.type.typeName.getText() === WITH_DEFAULT_PROPS_HELPER; | |
| if (alreadyHaveDefalutProps) return; | |
| if (options.useDefaultPropsHelper) insertWithDefaultPropsImport(); | |
| const indexOfTypeValue = ts.isIntersectionTypeNode(propsTypeAliasDeclaration.type) | |
| ? propsTypeAliasDeclaration.type.types.findIndex((typeEl) => ts.isTypeLiteralNode(typeEl)) | |
| : -1; | |
| const propTypesAreOnlyReferences = | |
| ts.isIntersectionTypeNode(propsTypeAliasDeclaration.type) && indexOfTypeValue === -1; | |
| const propsTypeValueNode = ts.isIntersectionTypeNode(propsTypeAliasDeclaration.type) | |
| ? ts.updateIntersectionTypeNode( | |
| propsTypeAliasDeclaration.type, | |
| ts.createNodeArray( | |
| propsTypeAliasDeclaration.type.types.filter( | |
| (_, k) => propTypesAreOnlyReferences || indexOfTypeValue === k, | |
| ), | |
| ), | |
| ) | |
| : propsTypeAliasDeclaration.type; | |
| const doesPropsTypeHaveExport = | |
| propsTypeAliasDeclaration.modifiers && | |
| propsTypeAliasDeclaration.modifiers.find( | |
| (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword, | |
| ); | |
| // rename type PropName -> type OwnPropName | |
| const updatedPropTypesName = doesPropsTypeHaveExport ? propsTypeName : `Own${propsTypeName}`; | |
| const updatedPropTypeAlias = ts.updateTypeAliasDeclaration( | |
| propsTypeAliasDeclaration, | |
| propsTypeAliasDeclaration.decorators, | |
| propsTypeAliasDeclaration.modifiers, | |
| ts.createIdentifier(updatedPropTypesName), | |
| propsTypeAliasDeclaration.typeParameters, | |
| propsTypeValueNode, | |
| ); | |
| const index = propsTypeAliasDeclaration.pos; | |
| const length = propsTypeAliasDeclaration.end - index; | |
| const text = printer.printNode(ts.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 | |
| ? ts.createTypeReferenceNode(WITH_DEFAULT_PROPS_HELPER, [ | |
| ts.createTypeReferenceNode(updatedPropTypesName, undefined), | |
| ts.createTypeQueryNode(ts.createIdentifier(defaultPropsTypeName)), | |
| ]) | |
| : ts.createIntersectionTypeNode([ | |
| ts.createTypeReferenceNode(updatedPropTypesName, undefined), | |
| ts.createTypeQueryNode(ts.createIdentifier(defaultPropsTypeName)), | |
| ]); | |
| const componentPropsTypeName = doesPropsTypeHaveExport | |
| ? `Private${propsTypeName}` | |
| : propsTypeName; | |
| const newPropsTypeAlias = ts.createTypeAliasDeclaration( | |
| undefined, | |
| undefined, | |
| ts.createIdentifier(componentPropsTypeName), | |
| undefined, | |
| ts.isIntersectionTypeNode(propsTypeAliasDeclaration.type) | |
| ? ts.createIntersectionTypeNode([ | |
| newPropsTypeValue, | |
| ...propsTypeAliasDeclaration.type.types.filter((el, k) => | |
| propTypesAreOnlyReferences | |
| ? ts.isIntersectionTypeNode(updatedPropTypeAlias) && | |
| !(updatedPropTypeAlias as ts.IntersectionTypeNode).types.includes(el) | |
| : indexOfTypeValue !== k, | |
| ), | |
| ]) | |
| : newPropsTypeValue, | |
| ); | |
| updates.push({ | |
| kind: 'insert', | |
| index: newTypeInsertPos, | |
| text: `\n\n${printer.printNode(ts.EmitHint.Unspecified, newPropsTypeAlias, sourceFile)}`, | |
| }); | |
| // we should rename component prop type in that case | |
| if (doesPropsTypeHaveExport) { | |
| const updatedComponentTypeReference = ts.updateTypeReferenceNode( | |
| componentTypeReference, | |
| ts.createIdentifier(componentPropsTypeName), | |
| undefined, | |
| ); | |
| const index = componentTypeReference.pos; | |
| const length = componentTypeReference.end - index; | |
| const text = printer.printNode( | |
| ts.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 as ts.BinaryExpression; | |
| const leftPart = expression.left as ts.PropertyAccessExpression; | |
| const componentName = leftPart.expression.getText(); | |
| const isObjectIdentifier = ts.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 && | |
| (ts.isArrowFunction(variableInitializer) || ts.isFunctionDeclaration(variableInitializer)) | |
| ? variableInitializer | |
| : undefined); | |
| // hasPropsTypeReference | |
| if ( | |
| componentDeclaration && | |
| componentDeclaration.parameters.length === 1 && | |
| componentDeclaration.parameters[0].type !== undefined && | |
| ts.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, | |
| ); | |
| } | |
| } | |
| }); | |
| classDefaultPropsAssignment.forEach((classDeclaration) => { | |
| const componentName = classDeclaration.name && classDeclaration.name.getText(); | |
| const defaultPropsDeclaration = classDeclaration.members | |
| .filter(ts.isPropertyDeclaration) | |
| .filter((declaration) => declaration.name.getText() === 'defaultProps')[0]; | |
| if (!defaultPropsDeclaration) return; | |
| const isObjectIdentifier = | |
| defaultPropsDeclaration.initializer && ts.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) => | |
| ts.isExpressionWithTypeArguments(x) && | |
| (x.typeArguments ? x.typeArguments.length > 0 : false), | |
| ) | |
| : undefined; | |
| const propsTypeReferenceNode = | |
| expressionWithTypeArguments && expressionWithTypeArguments.typeArguments | |
| ? (expressionWithTypeArguments.typeArguments.find((genericArgs) => | |
| ts.isTypeReferenceNode(genericArgs), | |
| ) as ts.TypeReferenceNode | undefined) | |
| : 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, | |
| ); | |
| } | |
| }); | |
| return updateSourceText(text, updates); | |
| }, | |
| }; | |
| // 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 ts.createImportDeclaration( | |
| undefined, | |
| undefined, | |
| ts.createImportClause( | |
| undefined, | |
| ts.createNamedImports([ | |
| ts.createImportSpecifier(undefined, ts.createIdentifier('WithDefaultProps')), | |
| ]), | |
| ), | |
| ts.createStringLiteral(':ts-utils/types/WithDefaultProps'), | |
| ); | |
| } | |
| export default reactDefaultPropsPlugin; |