Files
tsmorph/scripts/refactor-tsx.ts
2026-01-17 23:34:09 +00:00

306 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env ts-node
import { Project, SyntaxKind, SourceFile, Node } from 'ts-morph';
import * as path from 'path';
/**
* TSX Refactoring Script using ts-morph
*
* This script demonstrates how to use ts-morph to:
* 1. Analyze TSX files for large code blocks (>150 LOC)
* 2. Extract components and utilities to separate files
* 3. Automatically fix imports
*/
interface ExtractionCandidate {
name: string;
node: Node;
lineCount: number;
type: 'function' | 'component' | 'interface' | 'type' | 'variable';
}
class TSXRefactorer {
private project: Project;
private sourceFile: SourceFile;
constructor(filePath: string) {
this.project = new Project({
tsConfigFilePath: path.join(__dirname, '..', 'tsconfig.json'),
});
this.sourceFile = this.project.addSourceFileAtPath(filePath);
}
/**
* Analyze the file and identify extraction candidates
*/
analyzeFile(): ExtractionCandidate[] {
const candidates: ExtractionCandidate[] = [];
console.log('\n📊 Analyzing file:', this.sourceFile.getFilePath());
console.log('Total lines:', this.sourceFile.getEndLineNumber());
// Find all function declarations
const functions = this.sourceFile.getFunctions();
functions.forEach(func => {
const lineCount = this.getNodeLineCount(func);
if (lineCount > 20) { // Lower threshold for demonstration
candidates.push({
name: func.getName() || 'anonymous',
node: func,
lineCount,
type: 'function',
});
}
});
// Find all variable statements (includes arrow functions and hooks)
const variableStatements = this.sourceFile.getVariableStatements();
variableStatements.forEach(stmt => {
const declarations = stmt.getDeclarations();
declarations.forEach(decl => {
const lineCount = this.getNodeLineCount(decl);
const name = decl.getName();
if (lineCount > 10) {
candidates.push({
name,
node: decl,
lineCount,
type: 'variable',
});
}
});
});
// Find all interface declarations
const interfaces = this.sourceFile.getInterfaces();
interfaces.forEach(iface => {
candidates.push({
name: iface.getName(),
node: iface,
lineCount: this.getNodeLineCount(iface),
type: 'interface',
});
});
// Find all type aliases
const typeAliases = this.sourceFile.getTypeAliases();
typeAliases.forEach(typeAlias => {
candidates.push({
name: typeAlias.getName(),
node: typeAlias,
lineCount: this.getNodeLineCount(typeAlias),
type: 'type',
});
});
return candidates;
}
/**
* Extract types and interfaces to a separate file
*/
extractTypes(candidates: ExtractionCandidate[]): void {
const typeCandidates = candidates.filter(c =>
c.type === 'interface' || c.type === 'type'
);
if (typeCandidates.length === 0) {
console.log('\n⏭ No types to extract');
return;
}
console.log('\n🔄 Extracting types to separate file...');
const typesFilePath = this.sourceFile.getFilePath().replace('.tsx', '.types.ts');
const typesFile = this.project.createSourceFile(typesFilePath, '', { overwrite: true });
// Build the content for the types file
const typesContent = [
'/**',
' * Extracted types and interfaces',
' * Auto-generated by ts-morph refactoring script',
' */',
'',
...typeCandidates.map(candidate => {
const text = candidate.node.getText();
// Add export if not already present
return text.startsWith('export ') ? text : `export ${text}`;
}),
].join('\n');
// Write the content
typesFile.replaceWithText(typesContent);
typeCandidates.forEach(candidate => {
console.log(` ✓ Extracted: ${candidate.name} (${candidate.lineCount} lines)`);
});
// Remove from original file
typeCandidates.forEach(candidate => {
if (Node.isInterfaceDeclaration(candidate.node) || Node.isTypeAliasDeclaration(candidate.node)) {
candidate.node.remove();
}
});
// Add import to original file
const relativePath = './UserManagementDashboard.types';
const typeNames = typeCandidates.map(c => c.name);
this.sourceFile.addImportDeclaration({
moduleSpecifier: relativePath,
namedImports: typeNames,
});
typesFile.saveSync();
console.log(` 💾 Saved: ${path.basename(typesFilePath)}`);
}
/**
* Extract utility functions to a separate file
*/
extractUtilities(candidates: ExtractionCandidate[]): void {
const utilCandidates = candidates.filter(c =>
c.type === 'variable' && c.name.match(/^(validate|format|get|handle)/)
);
if (utilCandidates.length === 0) {
console.log('\n⏭ No utilities to extract');
return;
}
console.log('\n🔄 Extracting utility functions...');
const utilsFilePath = this.sourceFile.getFilePath().replace('.tsx', '.utils.ts');
const utilsFile = this.project.createSourceFile(utilsFilePath, '', { overwrite: true });
const extractedNames: string[] = [];
const utilStatements: string[] = [];
utilCandidates.forEach(candidate => {
const varDecl = candidate.node.asKindOrThrow(SyntaxKind.VariableDeclaration);
const varStatement = varDecl.getVariableStatement();
if (varStatement) {
// Get the full variable statement text
const text = varStatement.getText();
// Make it exported
const exportedText = text.replace(/^(const|let|var)/, 'export const');
utilStatements.push(exportedText);
extractedNames.push(candidate.name);
console.log(` ✓ Extracted: ${candidate.name} (${candidate.lineCount} lines)`);
// Remove from original
varStatement.remove();
}
});
// Build the complete utils file content
const utilsContent = [
'/**',
' * Extracted utility functions',
' * Auto-generated by ts-morph refactoring script',
' */',
'',
"import type { FormData, ValidationErrors, User } from './UserManagementDashboard.types';",
'',
...utilStatements,
].join('\n');
utilsFile.replaceWithText(utilsContent);
// Add import to original file
const relativePath = './UserManagementDashboard.utils';
this.sourceFile.addImportDeclaration({
moduleSpecifier: relativePath,
namedImports: extractedNames,
});
utilsFile.saveSync();
console.log(` 💾 Saved: ${path.basename(utilsFilePath)}`);
}
/**
* Generate a report about the file
*/
generateReport(candidates: ExtractionCandidate[]): void {
console.log('\n📋 Extraction Candidates Report');
console.log('================================');
const grouped = candidates.reduce((acc, c) => {
if (!acc[c.type]) acc[c.type] = [];
acc[c.type].push(c);
return acc;
}, {} as Record<string, ExtractionCandidate[]>);
Object.entries(grouped).forEach(([type, items]) => {
console.log(`\n${type.toUpperCase()}S (${items.length}):`);
items
.sort((a, b) => b.lineCount - a.lineCount)
.forEach(item => {
const indicator = item.lineCount > 50 ? '🔴' : item.lineCount > 20 ? '🟡' : '🟢';
console.log(` ${indicator} ${item.name}: ${item.lineCount} lines`);
});
});
const totalLines = candidates.reduce((sum, c) => sum + c.lineCount, 0);
console.log(`\n📊 Total lines in candidates: ${totalLines}`);
}
/**
* Save all changes
*/
save(): void {
this.sourceFile.saveSync();
console.log('\n✅ Refactoring complete!');
console.log('📁 Modified:', this.sourceFile.getFilePath());
}
/**
* Helper: Get line count for a node
*/
private getNodeLineCount(node: Node): number {
const start = node.getStartLineNumber();
const end = node.getEndLineNumber();
return end - start + 1;
}
}
// Main execution
async function main() {
console.log('🚀 TSX Refactoring Tool using ts-morph\n');
const targetFile = path.join(
__dirname,
'..',
'src',
'components',
'UserManagementDashboard.tsx'
);
const refactorer = new TSXRefactorer(targetFile);
// Step 1: Analyze
const candidates = refactorer.analyzeFile();
refactorer.generateReport(candidates);
// Step 2: Extract types
refactorer.extractTypes(candidates);
// Step 3: Extract utilities
refactorer.extractUtilities(candidates);
// Step 4: Save
refactorer.save();
console.log('\n💡 Next steps:');
console.log(' - Review the extracted files');
console.log(' - Consider extracting more components (UserForm, UserTable)');
console.log(' - Run npm run type-check to verify');
}
main().catch(console.error);