mirror of
https://github.com/johndoe6345789/tsmorph.git
synced 2026-04-24 13:54:58 +00:00
306 lines
8.7 KiB
TypeScript
306 lines
8.7 KiB
TypeScript
#!/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);
|