Add ts-morph refactoring tools with type analysis and lint fixing

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-17 23:34:09 +00:00
parent 6a2f1414dc
commit 7343af626a
12 changed files with 3686 additions and 75 deletions

27
.eslintrc.js Normal file
View File

@@ -0,0 +1,27 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-console': 'off',
},
env: {
browser: true,
node: true,
es6: true,
},
};

10
.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}

2505
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,13 @@
"build": "next build",
"start": "next start",
"lint": "eslint . --ext .ts,.tsx",
"type-check": "tsc --noEmit"
"lint:fix": "eslint . --ext .ts,.tsx --fix",
"format": "prettier --write \"**/*.{ts,tsx,json,md}\"",
"type-check": "tsc --noEmit",
"refactor": "ts-node scripts/refactor-tsx.ts",
"refactor:pass2": "ts-node scripts/refactor-tsx-pass2.ts",
"fix-lint": "ts-node scripts/fix-lint.ts",
"analyze-types": "ts-node scripts/analyze-types.ts"
},
"dependencies": {
"react": "^18.2.0",
@@ -21,6 +27,10 @@
"typescript": "^5.0.0",
"eslint": "^8.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0"
"@typescript-eslint/eslint-plugin": "^6.0.0",
"ts-morph": "^21.0.0",
"ts-node": "^10.9.0",
"prettier": "^3.0.0",
"eslint-config-prettier": "^9.0.0"
}
}

390
scripts/analyze-types.ts Normal file
View File

@@ -0,0 +1,390 @@
#!/usr/bin/env ts-node
import { Project, SourceFile, SyntaxKind, Type, Node } from 'ts-morph';
import * as path from 'path';
/**
* Type Analysis and Auto-fixing Tool using ts-morph
*
* This script:
* 1. Analyzes types in the refactored files
* 2. Adds missing type annotations where they can be inferred
* 3. Fixes incorrect or overly broad types (like 'any')
* 4. Adds return types to functions
* 5. Makes types more specific where possible
*/
class TypeAnalyzer {
private project: Project;
constructor() {
this.project = new Project({
tsConfigFilePath: path.join(__dirname, '..', 'tsconfig.json'),
compilerOptions: {
strict: true,
noImplicitAny: true,
},
});
}
/**
* Add return type annotations to functions that are missing them
*/
addReturnTypes(sourceFile: SourceFile): number {
let fixCount = 0;
// Find arrow functions without return types
const variableDeclarations = sourceFile.getVariableDeclarations();
variableDeclarations.forEach(decl => {
const initializer = decl.getInitializer();
if (initializer && Node.isArrowFunction(initializer)) {
const returnType = initializer.getReturnType();
const hasReturnType = initializer.getReturnTypeNode() !== undefined;
if (!hasReturnType && returnType) {
// Get the type text
const typeText = returnType.getText(initializer);
// Don't add 'void' or very complex types
if (typeText !== 'void' && typeText.length < 100) {
try {
initializer.setReturnType(typeText);
console.log(` ✓ Added return type to ${decl.getName()}: ${typeText}`);
fixCount++;
} catch (e) {
// Skip if it fails
}
}
}
}
});
// Find regular functions without return types
const functions = sourceFile.getFunctions();
functions.forEach(func => {
const hasReturnType = func.getReturnTypeNode() !== undefined;
if (!hasReturnType) {
const returnType = func.getReturnType();
const typeText = returnType.getText(func);
if (typeText !== 'void' && typeText.length < 100) {
try {
func.setReturnType(typeText);
console.log(` ✓ Added return type to ${func.getName()}: ${typeText}`);
fixCount++;
} catch (e) {
// Skip if it fails
}
}
}
});
return fixCount;
}
/**
* Add type annotations to parameters that are missing them
*/
addParameterTypes(sourceFile: SourceFile): number {
let fixCount = 0;
const functions = [
...sourceFile.getFunctions(),
...sourceFile.getVariableDeclarations()
.map(v => v.getInitializer())
.filter(init => init && Node.isArrowFunction(init))
.map(init => init as any),
];
functions.forEach(func => {
if (!func.getParameters) return;
const params = func.getParameters();
params.forEach((param: any) => {
const hasType = param.getTypeNode() !== undefined;
if (!hasType) {
const type = param.getType();
const typeText = type.getText(param);
// Only add if it's not 'any' and not too complex
if (typeText !== 'any' && typeText.length < 100) {
try {
param.setType(typeText);
console.log(` ✓ Added type to parameter ${param.getName()}: ${typeText}`);
fixCount++;
} catch (e) {
// Skip if it fails
}
}
}
});
});
return fixCount;
}
/**
* Replace 'any' types with more specific types where possible
*/
fixAnyTypes(sourceFile: SourceFile): number {
let fixCount = 0;
// Find all variables with 'any' type
const variableDeclarations = sourceFile.getVariableDeclarations();
variableDeclarations.forEach(decl => {
const typeNode = decl.getTypeNode();
if (typeNode && typeNode.getText() === 'any') {
// Try to infer a better type from the initializer
const initializer = decl.getInitializer();
if (initializer) {
const inferredType = initializer.getType();
const typeText = inferredType.getText(initializer);
if (typeText !== 'any' && typeText.length < 100) {
try {
decl.setType(typeText);
console.log(` ✓ Replaced 'any' with '${typeText}' for ${decl.getName()}`);
fixCount++;
} catch (e) {
// Skip if it fails
}
}
}
}
});
return fixCount;
}
/**
* Add missing type exports
*/
ensureTypeExports(sourceFile: SourceFile): number {
let fixCount = 0;
// Check if this is a .types.ts file
if (!sourceFile.getFilePath().includes('.types.ts')) {
return 0;
}
// Find all interfaces and type aliases
const interfaces = sourceFile.getInterfaces();
const typeAliases = sourceFile.getTypeAliases();
[...interfaces, ...typeAliases].forEach(type => {
if (!type.isExported()) {
type.setIsExported(true);
console.log(` ✓ Added export to type: ${type.getName()}`);
fixCount++;
}
});
return fixCount;
}
/**
* Analyze and report type issues
*/
analyzeTypes(sourceFile: SourceFile): void {
console.log(`\n📊 Type Analysis: ${path.basename(sourceFile.getFilePath())}`);
// Get diagnostics
const diagnostics = sourceFile.getPreEmitDiagnostics();
if (diagnostics.length === 0) {
console.log(' ✅ No type errors found');
} else {
console.log(` ⚠️ Found ${diagnostics.length} type issues:`);
diagnostics.slice(0, 5).forEach(diag => {
const message = diag.getMessageText();
const line = diag.getLineNumber();
console.log(` Line ${line}: ${message}`);
});
if (diagnostics.length > 5) {
console.log(` ... and ${diagnostics.length - 5} more`);
}
}
}
/**
* Improve type specificity
*/
improveTypeSpecificity(sourceFile: SourceFile): number {
let fixCount = 0;
// Find string literal types that could be more specific
const variableDeclarations = sourceFile.getVariableDeclarations();
variableDeclarations.forEach(decl => {
const initializer = decl.getInitializer();
const typeNode = decl.getTypeNode();
// If declared as 'string' but initializer is a string literal
if (typeNode && typeNode.getText() === 'string' && initializer) {
const initType = initializer.getType();
if (initType.isStringLiteral()) {
const literalValue = initType.getLiteralValue();
// Check if it's a const
const varStatement = decl.getVariableStatement();
if (varStatement && varStatement.getDeclarationKind() === 'const') {
// Can use literal type
try {
decl.setType(`'${literalValue}'`);
console.log(` ✓ Made type more specific for ${decl.getName()}: '${literalValue}'`);
fixCount++;
} catch (e) {
// Skip if it fails
}
}
}
}
});
return fixCount;
}
/**
* Process a single file
*/
processFile(filePath: string): void {
if (!require('fs').existsSync(filePath)) {
console.log(` ⏭️ File not found: ${filePath}`);
return;
}
console.log(`\n📄 Processing: ${path.basename(filePath)}`);
const sourceFile = this.project.addSourceFileAtPath(filePath);
let totalFixes = 0;
// Step 1: Ensure type exports
console.log(' 🔍 Checking type exports...');
totalFixes += this.ensureTypeExports(sourceFile);
// Step 2: Add return types
console.log(' 🔍 Adding missing return types...');
totalFixes += this.addReturnTypes(sourceFile);
// Step 3: Add parameter types
console.log(' 🔍 Adding missing parameter types...');
totalFixes += this.addParameterTypes(sourceFile);
// Step 4: Fix 'any' types
console.log(' 🔍 Fixing any types...');
totalFixes += this.fixAnyTypes(sourceFile);
// Step 5: Improve type specificity
console.log(' 🔍 Improving type specificity...');
totalFixes += this.improveTypeSpecificity(sourceFile);
if (totalFixes > 0) {
sourceFile.saveSync();
console.log(` 💾 Saved ${totalFixes} type improvements`);
} else {
console.log(' ✅ No type improvements needed');
}
// Step 6: Analyze remaining type issues
this.analyzeTypes(sourceFile);
}
/**
* Process all refactored files
*/
processAllFiles(): void {
const componentsDir = path.join(__dirname, '..', 'src', 'components');
const files = [
path.join(componentsDir, 'UserManagementDashboard.types.ts'),
path.join(componentsDir, 'UserManagementDashboard.utils.ts'),
path.join(componentsDir, 'UserManagementDashboard.tsx'),
];
files.forEach(file => this.processFile(file));
}
/**
* Generate type coverage report
*/
generateTypeCoverageReport(): void {
console.log('\n📈 Type Coverage Report');
console.log('========================\n');
const componentsDir = path.join(__dirname, '..', 'src', 'components');
const files = [
path.join(componentsDir, 'UserManagementDashboard.types.ts'),
path.join(componentsDir, 'UserManagementDashboard.utils.ts'),
path.join(componentsDir, 'UserManagementDashboard.tsx'),
];
files.forEach(filePath => {
if (!require('fs').existsSync(filePath)) return;
const sourceFile = this.project.addSourceFileAtPath(filePath);
const fileName = path.basename(filePath);
// Count typed vs untyped
const allDeclarations = [
...sourceFile.getVariableDeclarations(),
...sourceFile.getFunctions(),
];
let typedCount = 0;
let untypedCount = 0;
allDeclarations.forEach(decl => {
if ('getTypeNode' in decl) {
const hasType = decl.getTypeNode() !== undefined;
if (hasType) {
typedCount++;
} else {
untypedCount++;
}
}
});
const total = typedCount + untypedCount;
const coverage = total > 0 ? Math.round((typedCount / total) * 100) : 100;
console.log(`${fileName}:`);
console.log(` Type Coverage: ${coverage}% (${typedCount}/${total} declarations typed)`);
// Check for 'any' usage
const anyCount = sourceFile.getText().match(/:\s*any\b/g)?.length || 0;
if (anyCount > 0) {
console.log(` ⚠️ Contains ${anyCount} 'any' types`);
}
});
}
}
async function main() {
console.log('🚀 TypeScript Type Analysis and Auto-Fixing Tool\n');
const analyzer = new TypeAnalyzer();
// Process all files
analyzer.processAllFiles();
// Generate report
analyzer.generateTypeCoverageReport();
console.log('\n✅ Type analysis complete!');
console.log('\n💡 Next steps:');
console.log(' - Run npm run type-check to verify all types');
console.log(' - Review any remaining type issues');
}
main().catch(console.error);

162
scripts/fix-lint.ts Normal file
View File

@@ -0,0 +1,162 @@
#!/usr/bin/env ts-node
import { Project, SourceFile } from 'ts-morph';
import * as path from 'path';
import { execSync } from 'child_process';
/**
* Auto-fix linting issues in generated files
* This runs ESLint --fix and Prettier on all extracted files
*/
class LintFixer {
private project: Project;
constructor() {
this.project = new Project({
tsConfigFilePath: path.join(__dirname, '..', 'tsconfig.json'),
});
}
/**
* Format a file using Prettier
*/
formatFile(filePath: string): void {
console.log(` 🎨 Formatting: ${path.basename(filePath)}`);
try {
execSync(`npx prettier --write "${filePath}"`, {
cwd: path.join(__dirname, '..'),
stdio: 'pipe',
});
} catch (e) {
console.log(` ⚠️ Prettier warning (non-critical)`);
}
}
/**
* Fix linting issues in a file
*/
fixLinting(filePath: string): void {
console.log(` 🔧 Fixing lint issues: ${path.basename(filePath)}`);
try {
execSync(`npx eslint --fix "${filePath}"`, {
cwd: path.join(__dirname, '..'),
stdio: 'pipe',
});
} catch (e) {
// ESLint returns non-zero if there are unfixable issues
console.log(` ⚠️ Some lint issues may remain`);
}
}
/**
* Organize imports in a file using ts-morph
*/
organizeImports(filePath: string): void {
console.log(` 📦 Organizing imports: ${path.basename(filePath)}`);
const sourceFile = this.project.addSourceFileAtPath(filePath);
// Get all import declarations
const imports = sourceFile.getImportDeclarations();
// Sort imports: React first, then libraries, then local
const sortedImports = imports.sort((a, b) => {
const aModule = a.getModuleSpecifierValue();
const bModule = b.getModuleSpecifierValue();
// React imports first
if (aModule === 'react') return -1;
if (bModule === 'react') return 1;
// External packages (no . prefix)
const aIsExternal = !aModule.startsWith('.');
const bIsExternal = !bModule.startsWith('.');
if (aIsExternal && !bIsExternal) return -1;
if (!aIsExternal && bIsExternal) return 1;
// Alphabetical within groups
return aModule.localeCompare(bModule);
});
// Remove all imports
imports.forEach(imp => imp.remove());
// Add them back in sorted order
sortedImports.forEach((imp, index) => {
const structure = imp.getStructure();
sourceFile.insertImportDeclaration(index, structure);
});
sourceFile.saveSync();
}
/**
* Fix common TypeScript issues
*/
fixCommonIssues(filePath: string): void {
const sourceFile = this.project.addSourceFileAtPath(filePath);
// For now, skip unused import detection as it's complex
// Just ensure the file is valid
sourceFile.saveSync();
}
/**
* Process all component files
*/
processFiles(): void {
console.log('\n🔍 Auto-fixing lint issues in refactored files...\n');
const componentsDir = path.join(__dirname, '..', 'src', 'components');
const files = [
path.join(componentsDir, 'UserManagementDashboard.tsx'),
path.join(componentsDir, 'UserManagementDashboard.types.ts'),
path.join(componentsDir, 'UserManagementDashboard.utils.ts'),
];
files.forEach(file => {
if (require('fs').existsSync(file)) {
console.log(`\n📄 Processing: ${path.basename(file)}`);
// Step 1: Organize imports
try {
this.organizeImports(file);
} catch (e) {
console.log(` ⚠️ Could not organize imports: ${e}`);
}
// Step 2: Fix common issues
try {
this.fixCommonIssues(file);
} catch (e) {
console.log(` ⚠️ Could not fix common issues: ${e}`);
}
// Step 3: Format with Prettier
this.formatFile(file);
// Step 4: Fix with ESLint
this.fixLinting(file);
console.log(` ✅ Done`);
}
});
}
}
async function main() {
console.log('🚀 Lint Auto-Fixer for Refactored Files\n');
const fixer = new LintFixer();
fixer.processFiles();
console.log('\n✅ All files processed!');
console.log('\n💡 Next steps:');
console.log(' - Run npm run lint to check remaining issues');
console.log(' - Run npm run type-check to verify TypeScript');
}
main().catch(console.error);

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env ts-node
import { Project, SyntaxKind, SourceFile, VariableDeclaration } from 'ts-morph';
import * as path from 'path';
/**
* Second pass: Extract utility functions from inside the component
* This extracts helper functions like validateForm, getRoleBadgeColor, etc.
*/
class TSXRefactorer2 {
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);
}
/**
* Extract helper functions that don't use hooks or state
*/
extractHelperFunctions(): void {
console.log('\n🔄 Extracting helper functions (2nd pass)...');
const utilsFilePath = this.sourceFile.getFilePath().replace('.tsx', '.utils.ts');
// Find the main component
const componentDecl = this.sourceFile.getVariableDeclaration('UserManagementDashboard');
if (!componentDecl) {
console.log(' ⚠️ Could not find component');
return;
}
const arrowFunc = componentDecl.getInitializerIfKind(SyntaxKind.ArrowFunction);
if (!arrowFunc) {
console.log(' ⚠️ Component is not an arrow function');
return;
}
const body = arrowFunc.getBody();
if (!body || !body.isKind(SyntaxKind.Block)) {
console.log(' ⚠️ Component body not found');
return;
}
// Find helper functions inside the component
const helperFunctions: Array<{ name: string; text: string }> = [];
// Look for const declarations with arrow functions
const block = body.asKind(SyntaxKind.Block);
if (!block) return;
const statements = block.getStatements();
statements.forEach(stmt => {
if (stmt.isKind(SyntaxKind.VariableStatement)) {
const declarations = stmt.getDeclarations();
declarations.forEach(decl => {
const name = decl.getName();
const initializer = decl.getInitializer();
// Check if it's a helper function (arrow function that doesn't use hooks)
if (initializer && initializer.isKind(SyntaxKind.ArrowFunction)) {
const text = stmt.getText();
// Extract these specific helper functions
if (name.match(/^(validate|getRoleBadgeColor|getStatusBadgeColor|formatDate)/)) {
helperFunctions.push({ name, text });
}
}
});
}
});
if (helperFunctions.length === 0) {
console.log(' ⏭️ No helper functions to extract');
return;
}
// Read existing utils file or create new content
let utilsContent = '';
try {
const existingUtils = this.project.getSourceFile(utilsFilePath);
if (existingUtils) {
utilsContent = existingUtils.getFullText();
}
} catch (e) {
// File doesn't exist yet
utilsContent = [
'/**',
' * Extracted utility functions',
' * Auto-generated by ts-morph refactoring script',
' */',
'',
"import type { FormData, ValidationErrors, User } from './UserManagementDashboard.types';",
'',
].join('\n');
}
// Add the helper functions
const exportedFunctions = helperFunctions.map(func => {
// Make it exported
const exportedText = func.text.replace(/^(\s*)(const|let|var)/, '$1export const');
console.log(` ✓ Extracted: ${func.name}`);
return exportedText;
});
utilsContent += '\n' + exportedFunctions.join('\n\n');
// Write the utils file
const utilsFile = this.project.createSourceFile(utilsFilePath, utilsContent, { overwrite: true });
utilsFile.saveSync();
// Remove helper functions from component and add imports
const functionNames = helperFunctions.map(f => f.name);
statements.forEach(stmt => {
if (stmt.isKind(SyntaxKind.VariableStatement)) {
const declarations = stmt.getDeclarations();
declarations.forEach(decl => {
const name = decl.getName();
if (functionNames.includes(name)) {
stmt.remove();
}
});
}
});
// Add import
const existingImport = this.sourceFile.getImportDeclaration('./UserManagementDashboard.utils');
if (existingImport) {
// Remove existing and recreate with combined imports
const namedImports = existingImport.getNamedImports().map(ni => ni.getName());
const newImports = [...new Set([...namedImports, ...functionNames])];
existingImport.remove();
this.sourceFile.addImportDeclaration({
moduleSpecifier: './UserManagementDashboard.utils',
namedImports: newImports,
});
} else {
// Create new import
this.sourceFile.addImportDeclaration({
moduleSpecifier: './UserManagementDashboard.utils',
namedImports: functionNames,
});
}
this.sourceFile.saveSync();
console.log(` 💾 Saved: ${path.basename(utilsFilePath)}`);
}
save(): void {
this.sourceFile.saveSync();
console.log('\n✅ Second pass refactoring complete!');
}
}
async function main() {
console.log('🚀 TSX Refactoring Tool - Second Pass\n');
const targetFile = path.join(
__dirname,
'..',
'src',
'components',
'UserManagementDashboard.tsx'
);
const refactorer = new TSXRefactorer2(targetFile);
refactorer.extractHelperFunctions();
refactorer.save();
console.log('\n💡 Helper functions extracted!');
console.log(' - Run npm run type-check to verify');
}
main().catch(console.error);

305
scripts/refactor-tsx.ts Normal file
View File

@@ -0,0 +1,305 @@
#!/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);

16
scripts/tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"resolveJsonModule": true,
"moduleResolution": "node"
},
"ts-node": {
"compilerOptions": {
"module": "commonjs"
}
}
}

View File

@@ -1,33 +1,8 @@
import React, { useState, useEffect, useCallback } from 'react';
import { User, FormData, ValidationErrors } from "./UserManagementDashboard.types";
import { validateForm, getRoleBadgeColor, getStatusBadgeColor, formatDate } from "./UserManagementDashboard.utils";
/**
* BEFORE REFACTORING: Large Monolithic Component (~200+ LOC)
* This component handles user management with forms, tables, and API calls
* all in one file - a common anti-pattern that needs refactoring
*/
interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
status: 'active' | 'inactive';
createdAt: string;
}
interface FormData {
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
interface ValidationErrors {
name?: string;
email?: string;
role?: string;
}
export const UserManagementDashboard: React.FC = () => {
export const UserManagementDashboard: React.FC = (): React.JSX.Element => {
// State management
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
@@ -48,30 +23,6 @@ export const UserManagementDashboard: React.FC = () => {
const [formErrors, setFormErrors] = useState<ValidationErrors>({});
// Validation logic
const validateForm = (data: FormData): ValidationErrors => {
const errors: ValidationErrors = {};
if (!data.name.trim()) {
errors.name = 'Name is required';
} else if (data.name.length < 2) {
errors.name = 'Name must be at least 2 characters';
} else if (data.name.length > 50) {
errors.name = 'Name must be less than 50 characters';
}
if (!data.email.trim()) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.email = 'Invalid email format';
}
if (!data.role) {
errors.role = 'Role is required';
}
return errors;
};
// API simulation functions
const fetchUsers = useCallback(async () => {
setLoading(true);
@@ -225,27 +176,6 @@ export const UserManagementDashboard: React.FC = () => {
};
// Render helpers
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'admin': return '#ff6b6b';
case 'user': return '#4ecdc4';
case 'guest': return '#95a5a6';
default: return '#7f8c8d';
}
};
const getStatusBadgeColor = (status: string) => {
return status === 'active' ? '#2ecc71' : '#e74c3c';
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
return (
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
<h1>User Management Dashboard</h1>

View File

@@ -0,0 +1,23 @@
/**
* Extracted types and interfaces
* Auto-generated by ts-morph refactoring script
*/
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
status: 'active' | 'inactive';
createdAt: string;
}
export interface FormData {
name: string;
email: string;
role: 'admin' | 'user' | 'guest';
}
export interface ValidationErrors {
name?: string;
email?: string;
role?: string;
}

View File

@@ -0,0 +1,51 @@
/**
* Extracted utility functions
* Auto-generated by ts-morph refactoring script
*/
import type { FormData, ValidationErrors } from './UserManagementDashboard.types';
export const validateForm = (data: FormData): ValidationErrors => {
const errors: ValidationErrors = {};
if (!data.name.trim()) {
errors.name = 'Name is required';
} else if (data.name.length < 2) {
errors.name = 'Name must be at least 2 characters';
} else if (data.name.length > 50) {
errors.name = 'Name must be less than 50 characters';
}
if (!data.email.trim()) {
errors.email = 'Email is required';
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.email = 'Invalid email format';
}
if (!data.role) {
errors.role = 'Role is required';
}
return errors;
};
export const getRoleBadgeColor = (role: string): "#ff6b6b" | "#4ecdc4" | "#95a5a6" | "#7f8c8d" => {
switch (role) {
case 'admin': return '#ff6b6b';
case 'user': return '#4ecdc4';
case 'guest': return '#95a5a6';
default: return '#7f8c8d';
}
};
export const getStatusBadgeColor = (status: string): "#2ecc71" | "#e74c3c" => {
return status === 'active' ? '#2ecc71' : '#e74c3c';
};
export const formatDate = (dateString: string): string => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};