ntts
Version:
A CLI tool for refactoring an existing NodeJs application to a fully functional TypeScript application.
182 lines (172 loc) • 7.16 kB
text/typescript
import {
BinaryExpression,
ClassExpression,
ExpressionStatement,
Identifier,
Node,
SourceFile,
SyntaxKind,
VariableDeclarationKind,
} from 'ts-morph';
import ExportedVariableModel from '../../../models/exported-variable.model';
import VariableNameGenerator from '../../helpers/variable-name-generator/variable-name-generator';
import VariableCreator from '../helpers/variable-creator';
import ExportParser from '../helpers/export-parser';
import WriteAccessChecker from '../../helpers/write-access-checker/write-access-checker';
import {AccessExpressionKind} from '../../helpers/combined-types/combined-types';
class TopLevelRefactor {
static refactorTopLevelExport = (
exportName: string,
binary: BinaryExpression,
expression: ExpressionStatement,
accessExpression: AccessExpressionKind,
exportedVariables: ExportedVariableModel[],
usedNames: string[],
defaultExport: boolean,
sourceFile: SourceFile,
): ExportedVariableModel[] => {
const exported = ExportParser.exportVariableExists(exportName, exportedVariables, defaultExport);
const exportedNames = exportedVariables.map((e) => e.name);
if (exported && exported.directExport) {
return this.refactorExistingDirectExport(exported, usedNames.concat(exportedNames), binary, accessExpression, exportedVariables, sourceFile);
}
if (exported) {
this.refactorExistingExport(exported, accessExpression, sourceFile);
return exportedVariables;
}
if (Node.isIdentifier(binary.getRight())) {
const newExport = this.refactorIdentifierAssignment(exportName, binary, expression, usedNames.concat(exportedNames), defaultExport, sourceFile);
return exportedVariables.concat(newExport);
}
if (Node.isClassExpression(binary.getRight())) {
const newExport = this.refactorClassAssignment(exportName, binary, expression, usedNames.concat(exportedNames), defaultExport, sourceFile);
return exportedVariables.concat(newExport);
}
const newExport = this.refactorNewExport(exportName, binary, expression, usedNames.concat(exportedNames), defaultExport, sourceFile);
return exportedVariables.concat(newExport);
};
private static refactorExistingDirectExport = (
exported: ExportedVariableModel,
usedNames: string[],
binary: BinaryExpression,
accessExpression: AccessExpressionKind,
exportedVariables: ExportedVariableModel[],
sourceFile: SourceFile,
): ExportedVariableModel[] => {
const usableName = VariableNameGenerator.getUsableVariableName(exported.name, usedNames);
const index = ExportParser.getSourceFileIndex(binary);
VariableCreator.createVariable(usableName, index, exported.name, VariableDeclarationKind.Let, sourceFile);
accessExpression.replaceWithText(usableName);
return exportedVariables.map((variable) => {
if (variable.name === exported.name && variable.alias === exported.alias) {
return { name: usableName, alias: exported.alias || exported.name, defaultExport: exported.defaultExport };
}
return variable;
});
};
private static refactorExistingExport = (
exported: ExportedVariableModel,
accessExpression: AccessExpressionKind,
sourceFile: SourceFile,
) => {
sourceFile.getVariableStatementOrThrow(exported.name).setDeclarationKind(VariableDeclarationKind.Let);
accessExpression.replaceWithText(exported.name);
};
private static refactorClassAssignment = (
exportName: string,
binary: BinaryExpression,
expression: ExpressionStatement,
usedNames: string[],
defaultExport: boolean,
sourceFile: SourceFile,
): ExportedVariableModel => {
const assignedClass = binary.getRight().asKindOrThrow(SyntaxKind.ClassExpression);
const className = this.refactorClassExpressionExport(assignedClass, exportName, usedNames);
sourceFile.insertStatements(ExportParser.getSourceFileIndex(binary), assignedClass.getText());
expression.remove();
const exportModel: ExportedVariableModel = {
name: className,
directExport: true,
defaultExport,
};
return exportName === className ? exportModel : { ...exportModel, alias: exportName };
};
private static refactorIdentifierAssignment = (
exportName: string,
binary: BinaryExpression,
expression: ExpressionStatement,
usedNames: string[],
defaultExport: boolean,
sourceFile: SourceFile,
): ExportedVariableModel => {
const assignedIdentifier = binary.getRight().asKindOrThrow(SyntaxKind.Identifier);
if (this.canExportDirectly(assignedIdentifier, sourceFile)) {
const assignedName = assignedIdentifier.getText();
expression.remove();
const exportModel: ExportedVariableModel = {
name: assignedName,
directExport: true,
defaultExport,
};
return exportName === assignedName ? exportModel : { ...exportModel, alias: exportName };
}
const usableName = VariableNameGenerator.getUsableVariableName(exportName, usedNames);
VariableCreator.createVariable(
usableName,
ExportParser.getSourceFileIndex(binary),
binary.getRight().getText(),
VariableDeclarationKind.Const,
sourceFile,
);
expression.remove();
return { name: usableName, alias: exportName };
};
private static refactorNewExport = (
exportName: string,
binary: BinaryExpression,
expression: ExpressionStatement,
usedNames: string[],
defaultExport: boolean,
sourceFile: SourceFile,
): ExportedVariableModel => {
const usableName = VariableNameGenerator.getUsableVariableName(exportName, usedNames);
VariableCreator.createVariable(
usableName,
ExportParser.getSourceFileIndex(binary),
binary.getRight().getText(),
VariableDeclarationKind.Const,
sourceFile,
);
expression.remove();
return {
name: usableName,
alias: exportName !== usableName ? exportName : undefined,
defaultExport,
};
};
private static refactorClassExpressionExport = (classExpression: ClassExpression, exportName: string, usedNames: string[]): string => {
const className = classExpression.getName();
if (className && !usedNames.includes(className)) {
return className;
}
if (!usedNames.includes(exportName)) {
classExpression.rename(exportName);
return exportName;
}
const usableClassName = VariableNameGenerator.getUsableVariableName(`${exportName}Type`, usedNames);
classExpression.rename(usableClassName);
return usableClassName;
};
private static canExportDirectly = (identifier: Identifier, sourceFile: SourceFile): boolean => {
const nodeName = identifier.getText();
const implementation = sourceFile.getVariableDeclaration(nodeName) || sourceFile.getFunction(nodeName) || sourceFile.getClass(nodeName);
if (Node.isFunctionDeclaration(implementation) || Node.isClassDeclaration(implementation)) {
return true;
}
if (Node.isVariableDeclaration(implementation)) {
return !WriteAccessChecker.hasValueChanged(implementation);
}
return false;
};
}
export default TopLevelRefactor;