mirror of
https://github.com/johndoe6345789/tsmorph.git
synced 2026-04-24 13:54:58 +00:00
Clarify variable LOC in docs
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# TSX Refactoring with ts-morph
|
||||
|
||||
Automated TSX/TypeScript refactoring tools using [ts-morph](https://ts-morph.com/) to extract large code blocks (>150 LOC) into separate, well-typed, lint-compliant files with automatic import management.
|
||||
Automated TSX/TypeScript refactoring tools using [ts-morph](https://ts-morph.com/) to extract code blocks into separate, well-typed, lint-compliant files with automatic import management. The intent is to make smaller components practical (e.g., 50/100/150 LOC thresholds) by pushing logic into hooks/utilities and iterating via a feedback loop.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -8,6 +8,8 @@ Automated TSX/TypeScript refactoring tools using [ts-morph](https://ts-morph.com
|
||||
- Extracts types and interfaces to separate `.types.ts` files
|
||||
- Extracts utility functions to separate `.utils.ts` files
|
||||
- Automatically generates and fixes imports
|
||||
- Supports smaller component targets (e.g., 50/100/150 LOC) by pushing logic into hooks and helpers
|
||||
- Designed for iterative runs so each pass can shrink the component further
|
||||
|
||||
🔍 **Type Analysis & Auto-Fixing**
|
||||
- Infers and adds missing return types
|
||||
@@ -80,9 +82,19 @@ npm run type-check
|
||||
| `npm run lint` | Check for lint issues |
|
||||
| `npm run format` | Format all files with Prettier |
|
||||
|
||||
### CLI Options (Target Any App)
|
||||
|
||||
Use the scripts directly for non-demo components:
|
||||
|
||||
```bash
|
||||
ts-node scripts/refactor-tsx.ts --file path/to/Dashboard.tsx --min-function-lines 50 --min-variable-lines 25
|
||||
ts-node scripts/refactor-tsx-pass2.ts --file path/to/Dashboard.tsx --helper-pattern "^(validate|get|format|handle)"
|
||||
ts-node scripts/analyze-types.ts --files path/to/Dashboard.types.ts,path/to/Dashboard.utils.ts,path/to/Dashboard.tsx
|
||||
```
|
||||
|
||||
## Refactoring Examples
|
||||
|
||||
### Before: Monolithic Component (603 LOC)
|
||||
### Before: Monolithic Component
|
||||
|
||||
```tsx
|
||||
// UserManagementDashboard.tsx - Everything in one file
|
||||
@@ -169,7 +181,7 @@ export const formatDate = (dateString: string): string => {
|
||||
};
|
||||
```
|
||||
|
||||
**UserManagementDashboard.tsx** (532 LOC)
|
||||
**UserManagementDashboard.tsx** (size varies)
|
||||
```tsx
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { User, FormData, ValidationErrors } from './UserManagementDashboard.types';
|
||||
|
||||
19
README.md
19
README.md
@@ -1,6 +1,6 @@
|
||||
# tsmorph
|
||||
|
||||
Automated TSX/TypeScript refactoring toolkit using [ts-morph](https://ts-morph.com/) to intelligently extract large code blocks (>150 LOC) into separate, well-typed, lint-compliant files with automatic import management.
|
||||
Automated TSX/TypeScript refactoring toolkit using [ts-morph](https://ts-morph.com/) to intelligently extract code blocks into separate, well-typed, lint-compliant files with automatic import management. The goal is to support smaller components (e.g., 50/100/150 LOC thresholds) and push more logic into hooks/utilities via a feedback loop.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
@@ -19,6 +19,8 @@ npm run type-check # Verify everything compiles
|
||||
## ✨ Features
|
||||
|
||||
- **Automated Code Extraction**: Extracts types, interfaces, and utility functions to separate files
|
||||
- **Smaller Component Targets**: Supports tighter LOC targets (e.g., 50/100/150) by encouraging extraction into hooks and utilities
|
||||
- **Feedback Loop Friendly**: Re-run the pipeline to keep shrinking components after each extraction pass
|
||||
- **Smart Import Management**: Automatically adds, removes, and organizes imports
|
||||
- **Type Analysis**: Infers and adds missing return types, parameter types, and fixes `any` types
|
||||
- **Lint Auto-Fixing**: Integrates with ESLint and Prettier for consistent code quality
|
||||
@@ -26,11 +28,11 @@ npm run type-check # Verify everything compiles
|
||||
|
||||
## 📊 Results
|
||||
|
||||
Starting with a monolithic **603-line** TSX component, the tools automatically refactor it into:
|
||||
Starting with a monolithic TSX component, the tools automatically refactor it into:
|
||||
|
||||
| File | LOC | Purpose |
|
||||
|------|-----|---------|
|
||||
| `UserManagementDashboard.tsx` | 532 | Main component logic |
|
||||
| `UserManagementDashboard.tsx` | Varies | Main component logic |
|
||||
| `UserManagementDashboard.types.ts` | 22 | Type definitions & interfaces |
|
||||
| `UserManagementDashboard.utils.ts` | 50 | Utility functions with precise types |
|
||||
|
||||
@@ -61,6 +63,16 @@ See [DOCUMENTATION.md](./DOCUMENTATION.md) for comprehensive guides on:
|
||||
| `npm run lint` | Check for lint issues |
|
||||
| `npm run format` | Format all files with Prettier |
|
||||
|
||||
## 🧰 CLI Usage for Another App
|
||||
|
||||
Target any TSX file by passing file paths and thresholds directly to the scripts:
|
||||
|
||||
```bash
|
||||
ts-node scripts/refactor-tsx.ts --file path/to/Dashboard.tsx --min-function-lines 50 --min-variable-lines 25
|
||||
ts-node scripts/refactor-tsx-pass2.ts --file path/to/Dashboard.tsx --helper-pattern "^(validate|get|format|handle)"
|
||||
ts-node scripts/analyze-types.ts --files path/to/Dashboard.types.ts,path/to/Dashboard.utils.ts,path/to/Dashboard.tsx
|
||||
```
|
||||
|
||||
## 📦 Example Output
|
||||
|
||||
### Type Analysis Report
|
||||
@@ -86,4 +98,3 @@ See [DOCUMENTATION.md](./DOCUMENTATION.md) for comprehensive guides on:
|
||||
## 📝 License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
import { Project, SourceFile, SyntaxKind, Type, Node } from 'ts-morph';
|
||||
import { Project, SourceFile, Node } from 'ts-morph';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
@@ -14,6 +14,51 @@ import * as path from 'path';
|
||||
* 5. Makes types more specific where possible
|
||||
*/
|
||||
|
||||
interface CliArgs {
|
||||
files: string[];
|
||||
help?: boolean;
|
||||
}
|
||||
|
||||
const parseArgs = (args: string[]): CliArgs => {
|
||||
const result: CliArgs = { files: [] };
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (!arg) continue;
|
||||
|
||||
switch (arg) {
|
||||
case '--files': {
|
||||
const next = args[i + 1];
|
||||
if (next) {
|
||||
result.files.push(...next.split(',').map(entry => entry.trim()).filter(Boolean));
|
||||
i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '--file': {
|
||||
const next = args[i + 1];
|
||||
if (next) {
|
||||
result.files.push(next);
|
||||
i += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case '--help':
|
||||
case '-h':
|
||||
result.help = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const printUsage = (): void => {
|
||||
console.log(`\nUsage:\n ts-node scripts/analyze-types.ts [options]\n\nOptions:\n --file <path> Single file to analyze (repeatable)\n --files <a,b,c> Comma-separated list of files to analyze\n --help Show this help message\n`);
|
||||
};
|
||||
|
||||
class TypeAnalyzer {
|
||||
private project: Project;
|
||||
|
||||
@@ -304,30 +349,19 @@ class TypeAnalyzer {
|
||||
/**
|
||||
* 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'),
|
||||
];
|
||||
|
||||
processAllFiles(filePaths?: string[]): void {
|
||||
const files = filePaths && filePaths.length > 0 ? filePaths : this.getDefaultFiles();
|
||||
files.forEach(file => this.processFile(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate type coverage report
|
||||
*/
|
||||
generateTypeCoverageReport(): void {
|
||||
generateTypeCoverageReport(filePaths?: string[]): 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'),
|
||||
];
|
||||
const files = filePaths && filePaths.length > 0 ? filePaths : this.getDefaultFiles();
|
||||
|
||||
files.forEach(filePath => {
|
||||
if (!require('fs').existsSync(filePath)) return;
|
||||
@@ -368,18 +402,34 @@ class TypeAnalyzer {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getDefaultFiles(): string[] {
|
||||
const componentsDir = path.join(__dirname, '..', 'src', 'components');
|
||||
return [
|
||||
path.join(componentsDir, 'UserManagementDashboard.types.ts'),
|
||||
path.join(componentsDir, 'UserManagementDashboard.utils.ts'),
|
||||
path.join(componentsDir, 'UserManagementDashboard.tsx'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 TypeScript Type Analysis and Auto-Fixing Tool\n');
|
||||
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help) {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
const analyzer = new TypeAnalyzer();
|
||||
|
||||
// Process all files
|
||||
analyzer.processAllFiles();
|
||||
const files = args.files.map(file => path.resolve(file));
|
||||
analyzer.processAllFiles(files);
|
||||
|
||||
// Generate report
|
||||
analyzer.generateTypeCoverageReport();
|
||||
analyzer.generateTypeCoverageReport(files);
|
||||
|
||||
console.log('\n✅ Type analysis complete!');
|
||||
console.log('\n💡 Next steps:');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env ts-node
|
||||
|
||||
import { Project, SyntaxKind, SourceFile, VariableDeclaration } from 'ts-morph';
|
||||
import { Project, SyntaxKind, SourceFile } from 'ts-morph';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
@@ -8,16 +8,93 @@ import * as path from 'path';
|
||||
* This extracts helper functions like validateForm, getRoleBadgeColor, etc.
|
||||
*/
|
||||
|
||||
interface Pass2Options {
|
||||
targetFile: string;
|
||||
utilsFile: string;
|
||||
typesFile: string;
|
||||
helperNamePattern: RegExp;
|
||||
}
|
||||
|
||||
interface CliArgs {
|
||||
file?: string;
|
||||
utils?: string;
|
||||
types?: string;
|
||||
helperPattern?: string;
|
||||
help?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_HELPER_PATTERN = '^(validate|get|format|handle)';
|
||||
|
||||
const toModuleSpecifier = (fromFile: string, toFile: string): string => {
|
||||
const relativePath = path
|
||||
.relative(path.dirname(fromFile), toFile)
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\.(tsx?|jsx?)$/, '');
|
||||
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
||||
};
|
||||
|
||||
const resolveOutputPath = (inputFile: string, suffix: string): string => {
|
||||
if (inputFile.endsWith('.tsx')) {
|
||||
return inputFile.replace(/\.tsx$/, suffix);
|
||||
}
|
||||
if (inputFile.endsWith('.ts')) {
|
||||
return inputFile.replace(/\.ts$/, suffix);
|
||||
}
|
||||
return `${inputFile}${suffix}`;
|
||||
};
|
||||
|
||||
const parseArgs = (args: string[]): CliArgs => {
|
||||
const result: CliArgs = {};
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (!arg) continue;
|
||||
|
||||
switch (arg) {
|
||||
case '--file':
|
||||
result.file = args[i + 1];
|
||||
i += 1;
|
||||
break;
|
||||
case '--utils':
|
||||
result.utils = args[i + 1];
|
||||
i += 1;
|
||||
break;
|
||||
case '--types':
|
||||
result.types = args[i + 1];
|
||||
i += 1;
|
||||
break;
|
||||
case '--helper-pattern':
|
||||
result.helperPattern = args[i + 1];
|
||||
i += 1;
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
result.help = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const printUsage = (): void => {
|
||||
console.log(`\nUsage:\n ts-node scripts/refactor-tsx-pass2.ts --file <path> [options]\n\nOptions:\n --utils <path> Output utils file path (default: <file>.utils.ts)\n --types <path> Types file path for type-only imports\n --helper-pattern <regex> Regex for helper function names (default: ${DEFAULT_HELPER_PATTERN})\n --help Show this help message\n`);
|
||||
};
|
||||
|
||||
class TSXRefactorer2 {
|
||||
private project: Project;
|
||||
private sourceFile: SourceFile;
|
||||
private options: Pass2Options;
|
||||
|
||||
constructor(filePath: string) {
|
||||
constructor(options: Pass2Options) {
|
||||
this.project = new Project({
|
||||
tsConfigFilePath: path.join(__dirname, '..', 'tsconfig.json'),
|
||||
});
|
||||
|
||||
this.sourceFile = this.project.addSourceFileAtPath(filePath);
|
||||
this.sourceFile = this.project.addSourceFileAtPath(options.targetFile);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,10 +103,13 @@ class TSXRefactorer2 {
|
||||
extractHelperFunctions(): void {
|
||||
console.log('\n🔄 Extracting helper functions (2nd pass)...');
|
||||
|
||||
const utilsFilePath = this.sourceFile.getFilePath().replace('.tsx', '.utils.ts');
|
||||
const utilsFilePath = this.options.utilsFile;
|
||||
|
||||
// Find the main component
|
||||
const componentDecl = this.sourceFile.getVariableDeclaration('UserManagementDashboard');
|
||||
const componentDecl = this.sourceFile.getVariableDeclarations().find(decl => {
|
||||
const initializer = decl.getInitializer();
|
||||
return initializer?.isKind(SyntaxKind.ArrowFunction);
|
||||
});
|
||||
if (!componentDecl) {
|
||||
console.log(' ⚠️ Could not find component');
|
||||
return;
|
||||
@@ -69,7 +149,7 @@ class TSXRefactorer2 {
|
||||
const text = stmt.getText();
|
||||
|
||||
// Extract these specific helper functions
|
||||
if (name.match(/^(validate|getRoleBadgeColor|getStatusBadgeColor|formatDate)/)) {
|
||||
if (this.options.helperNamePattern.test(name)) {
|
||||
helperFunctions.push({ name, text });
|
||||
}
|
||||
}
|
||||
@@ -97,9 +177,9 @@ class TSXRefactorer2 {
|
||||
' * Auto-generated by ts-morph refactoring script',
|
||||
' */',
|
||||
'',
|
||||
"import type { FormData, ValidationErrors, User } from './UserManagementDashboard.types';",
|
||||
this.getTypeImportStatement(utilsFilePath),
|
||||
'',
|
||||
].join('\n');
|
||||
].filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
// Add the helper functions
|
||||
@@ -132,20 +212,19 @@ class TSXRefactorer2 {
|
||||
});
|
||||
|
||||
// Add import
|
||||
const existingImport = this.sourceFile.getImportDeclaration('./UserManagementDashboard.utils');
|
||||
const moduleSpecifier = toModuleSpecifier(this.options.targetFile, utilsFilePath);
|
||||
const existingImport = this.sourceFile.getImportDeclaration(moduleSpecifier);
|
||||
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',
|
||||
moduleSpecifier,
|
||||
namedImports: newImports,
|
||||
});
|
||||
} else {
|
||||
// Create new import
|
||||
this.sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: './UserManagementDashboard.utils',
|
||||
moduleSpecifier,
|
||||
namedImports: functionNames,
|
||||
});
|
||||
}
|
||||
@@ -158,20 +237,60 @@ class TSXRefactorer2 {
|
||||
this.sourceFile.saveSync();
|
||||
console.log('\n✅ Second pass refactoring complete!');
|
||||
}
|
||||
|
||||
private getExportedTypeNames(): string[] {
|
||||
if (!this.options.typesFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const typesFile = this.project.getSourceFile(this.options.typesFile);
|
||||
if (!typesFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const interfaces = typesFile.getInterfaces();
|
||||
const typeAliases = typesFile.getTypeAliases();
|
||||
|
||||
return [...interfaces, ...typeAliases]
|
||||
.filter(type => type.isExported())
|
||||
.map(type => type.getName());
|
||||
}
|
||||
|
||||
private getTypeImportStatement(utilsFilePath: string): string | null {
|
||||
if (!this.options.typesFile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const typeNames = this.getExportedTypeNames();
|
||||
if (typeNames.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `import type { ${typeNames.join(', ')} } from '${toModuleSpecifier(utilsFilePath, this.options.typesFile)}';`;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🚀 TSX Refactoring Tool - Second Pass\n');
|
||||
|
||||
const targetFile = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'components',
|
||||
'UserManagementDashboard.tsx'
|
||||
);
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help) {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
const refactorer = new TSXRefactorer2(targetFile);
|
||||
const targetFile = args.file
|
||||
? path.resolve(args.file)
|
||||
: path.join(__dirname, '..', 'src', 'components', 'UserManagementDashboard.tsx');
|
||||
|
||||
const typesFile = args.types ? path.resolve(args.types) : resolveOutputPath(targetFile, '.types.ts');
|
||||
|
||||
const refactorer = new TSXRefactorer2({
|
||||
targetFile,
|
||||
utilsFile: path.resolve(args.utils ?? resolveOutputPath(targetFile, '.utils.ts')),
|
||||
typesFile,
|
||||
helperNamePattern: new RegExp(args.helperPattern ?? DEFAULT_HELPER_PATTERN),
|
||||
});
|
||||
refactorer.extractHelperFunctions();
|
||||
refactorer.save();
|
||||
|
||||
|
||||
@@ -19,16 +19,107 @@ interface ExtractionCandidate {
|
||||
type: 'function' | 'component' | 'interface' | 'type' | 'variable';
|
||||
}
|
||||
|
||||
interface RefactorOptions {
|
||||
targetFile: string;
|
||||
typesFile: string;
|
||||
utilsFile: string;
|
||||
minFunctionLines: number;
|
||||
minVariableLines: number;
|
||||
utilNamePattern: RegExp;
|
||||
}
|
||||
|
||||
interface CliArgs {
|
||||
file?: string;
|
||||
types?: string;
|
||||
utils?: string;
|
||||
minFunctionLines?: number;
|
||||
minVariableLines?: number;
|
||||
utilPattern?: string;
|
||||
help?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_MIN_FUNCTION_LINES = 20;
|
||||
const DEFAULT_MIN_VARIABLE_LINES = 10;
|
||||
const DEFAULT_UTIL_PATTERN = '^(validate|format|get|handle)';
|
||||
|
||||
const toModuleSpecifier = (fromFile: string, toFile: string): string => {
|
||||
const relativePath = path
|
||||
.relative(path.dirname(fromFile), toFile)
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\.(tsx?|jsx?)$/, '');
|
||||
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
||||
};
|
||||
|
||||
const parseArgs = (args: string[]): CliArgs => {
|
||||
const result: CliArgs = {};
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (!arg) continue;
|
||||
|
||||
switch (arg) {
|
||||
case '--file':
|
||||
result.file = args[i + 1];
|
||||
i += 1;
|
||||
break;
|
||||
case '--types':
|
||||
result.types = args[i + 1];
|
||||
i += 1;
|
||||
break;
|
||||
case '--utils':
|
||||
result.utils = args[i + 1];
|
||||
i += 1;
|
||||
break;
|
||||
case '--min-function-lines':
|
||||
result.minFunctionLines = Number(args[i + 1]);
|
||||
i += 1;
|
||||
break;
|
||||
case '--min-variable-lines':
|
||||
result.minVariableLines = Number(args[i + 1]);
|
||||
i += 1;
|
||||
break;
|
||||
case '--util-pattern':
|
||||
result.utilPattern = args[i + 1];
|
||||
i += 1;
|
||||
break;
|
||||
case '--help':
|
||||
case '-h':
|
||||
result.help = true;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const printUsage = (): void => {
|
||||
console.log(`\nUsage:\n ts-node scripts/refactor-tsx.ts --file <path> [options]\n\nOptions:\n --types <path> Output types file path (default: <file>.types.ts)\n --utils <path> Output utils file path (default: <file>.utils.ts)\n --min-function-lines <num> Minimum lines before extracting functions (default: ${DEFAULT_MIN_FUNCTION_LINES})\n --min-variable-lines <num> Minimum lines before extracting variables (default: ${DEFAULT_MIN_VARIABLE_LINES})\n --util-pattern <regex> Regex for utility names (default: ${DEFAULT_UTIL_PATTERN})\n --help Show this help message\n`);
|
||||
};
|
||||
|
||||
const resolveOutputPath = (inputFile: string, suffix: string): string => {
|
||||
if (inputFile.endsWith('.tsx')) {
|
||||
return inputFile.replace(/\.tsx$/, suffix);
|
||||
}
|
||||
if (inputFile.endsWith('.ts')) {
|
||||
return inputFile.replace(/\.ts$/, suffix);
|
||||
}
|
||||
return `${inputFile}${suffix}`;
|
||||
};
|
||||
|
||||
class TSXRefactorer {
|
||||
private project: Project;
|
||||
private sourceFile: SourceFile;
|
||||
private options: RefactorOptions;
|
||||
|
||||
constructor(filePath: string) {
|
||||
constructor(options: RefactorOptions) {
|
||||
this.project = new Project({
|
||||
tsConfigFilePath: path.join(__dirname, '..', 'tsconfig.json'),
|
||||
});
|
||||
|
||||
this.sourceFile = this.project.addSourceFileAtPath(filePath);
|
||||
this.sourceFile = this.project.addSourceFileAtPath(options.targetFile);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,7 +135,7 @@ class TSXRefactorer {
|
||||
const functions = this.sourceFile.getFunctions();
|
||||
functions.forEach(func => {
|
||||
const lineCount = this.getNodeLineCount(func);
|
||||
if (lineCount > 20) { // Lower threshold for demonstration
|
||||
if (lineCount > this.options.minFunctionLines) {
|
||||
candidates.push({
|
||||
name: func.getName() || 'anonymous',
|
||||
node: func,
|
||||
@@ -62,7 +153,7 @@ class TSXRefactorer {
|
||||
const lineCount = this.getNodeLineCount(decl);
|
||||
const name = decl.getName();
|
||||
|
||||
if (lineCount > 10) {
|
||||
if (lineCount > this.options.minVariableLines) {
|
||||
candidates.push({
|
||||
name,
|
||||
node: decl,
|
||||
@@ -113,7 +204,7 @@ class TSXRefactorer {
|
||||
|
||||
console.log('\n🔄 Extracting types to separate file...');
|
||||
|
||||
const typesFilePath = this.sourceFile.getFilePath().replace('.tsx', '.types.ts');
|
||||
const typesFilePath = this.options.typesFile;
|
||||
const typesFile = this.project.createSourceFile(typesFilePath, '', { overwrite: true });
|
||||
|
||||
// Build the content for the types file
|
||||
@@ -145,13 +236,11 @@ class TSXRefactorer {
|
||||
});
|
||||
|
||||
// Add import to original file
|
||||
const relativePath = './UserManagementDashboard.types';
|
||||
const typeNames = typeCandidates.map(c => c.name);
|
||||
|
||||
this.sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: relativePath,
|
||||
namedImports: typeNames,
|
||||
});
|
||||
if (typeNames.length > 0) {
|
||||
const moduleSpecifier = toModuleSpecifier(this.options.targetFile, typesFilePath);
|
||||
this.addOrUpdateNamedImport(moduleSpecifier, typeNames);
|
||||
}
|
||||
|
||||
typesFile.saveSync();
|
||||
console.log(` 💾 Saved: ${path.basename(typesFilePath)}`);
|
||||
@@ -162,7 +251,7 @@ class TSXRefactorer {
|
||||
*/
|
||||
extractUtilities(candidates: ExtractionCandidate[]): void {
|
||||
const utilCandidates = candidates.filter(c =>
|
||||
c.type === 'variable' && c.name.match(/^(validate|format|get|handle)/)
|
||||
c.type === 'variable' && this.options.utilNamePattern.test(c.name)
|
||||
);
|
||||
|
||||
if (utilCandidates.length === 0) {
|
||||
@@ -172,7 +261,7 @@ class TSXRefactorer {
|
||||
|
||||
console.log('\n🔄 Extracting utility functions...');
|
||||
|
||||
const utilsFilePath = this.sourceFile.getFilePath().replace('.tsx', '.utils.ts');
|
||||
const utilsFilePath = this.options.utilsFile;
|
||||
const utilsFile = this.project.createSourceFile(utilsFilePath, '', { overwrite: true });
|
||||
|
||||
const extractedNames: string[] = [];
|
||||
@@ -199,25 +288,28 @@ class TSXRefactorer {
|
||||
});
|
||||
|
||||
// Build the complete utils file content
|
||||
const typeNames = this.getExportedTypeNames();
|
||||
const typeImport = typeNames.length > 0
|
||||
? `import type { ${typeNames.join(', ')} } from '${toModuleSpecifier(utilsFilePath, this.options.typesFile)}';`
|
||||
: null;
|
||||
|
||||
const utilsContent = [
|
||||
'/**',
|
||||
' * Extracted utility functions',
|
||||
' * Auto-generated by ts-morph refactoring script',
|
||||
' */',
|
||||
'',
|
||||
"import type { FormData, ValidationErrors, User } from './UserManagementDashboard.types';",
|
||||
'',
|
||||
...(typeImport ? [typeImport, ''] : []),
|
||||
...utilStatements,
|
||||
].join('\n');
|
||||
|
||||
utilsFile.replaceWithText(utilsContent);
|
||||
|
||||
// Add import to original file
|
||||
const relativePath = './UserManagementDashboard.utils';
|
||||
this.sourceFile.addImportDeclaration({
|
||||
moduleSpecifier: relativePath,
|
||||
namedImports: extractedNames,
|
||||
});
|
||||
if (extractedNames.length > 0) {
|
||||
const moduleSpecifier = toModuleSpecifier(this.options.targetFile, utilsFilePath);
|
||||
this.addOrUpdateNamedImport(moduleSpecifier, extractedNames);
|
||||
}
|
||||
|
||||
utilsFile.saveSync();
|
||||
console.log(` 💾 Saved: ${path.basename(utilsFilePath)}`);
|
||||
@@ -267,21 +359,69 @@ class TSXRefactorer {
|
||||
const end = node.getEndLineNumber();
|
||||
return end - start + 1;
|
||||
}
|
||||
|
||||
private addOrUpdateNamedImport(moduleSpecifier: string, names: string[]): void {
|
||||
const existingImport = this.sourceFile.getImportDeclaration(moduleSpecifier);
|
||||
if (existingImport) {
|
||||
const existingNames = existingImport.getNamedImports().map(imp => imp.getName());
|
||||
const mergedNames = Array.from(new Set([...existingNames, ...names]));
|
||||
existingImport.remove();
|
||||
this.sourceFile.addImportDeclaration({
|
||||
moduleSpecifier,
|
||||
namedImports: mergedNames,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.sourceFile.addImportDeclaration({
|
||||
moduleSpecifier,
|
||||
namedImports: names,
|
||||
});
|
||||
}
|
||||
|
||||
private getExportedTypeNames(): string[] {
|
||||
const typesFile = this.project.getSourceFile(this.options.typesFile);
|
||||
if (!typesFile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const interfaces = typesFile.getInterfaces();
|
||||
const typeAliases = typesFile.getTypeAliases();
|
||||
|
||||
return [...interfaces, ...typeAliases]
|
||||
.filter(type => type.isExported())
|
||||
.map(type => type.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
async function main() {
|
||||
console.log('🚀 TSX Refactoring Tool using ts-morph\n');
|
||||
|
||||
const targetFile = path.join(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'components',
|
||||
'UserManagementDashboard.tsx'
|
||||
);
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.help) {
|
||||
printUsage();
|
||||
return;
|
||||
}
|
||||
|
||||
const refactorer = new TSXRefactorer(targetFile);
|
||||
const targetFile = args.file
|
||||
? path.resolve(args.file)
|
||||
: path.join(__dirname, '..', 'src', 'components', 'UserManagementDashboard.tsx');
|
||||
|
||||
const options: RefactorOptions = {
|
||||
targetFile,
|
||||
typesFile: path.resolve(args.types ?? resolveOutputPath(targetFile, '.types.ts')),
|
||||
utilsFile: path.resolve(args.utils ?? resolveOutputPath(targetFile, '.utils.ts')),
|
||||
minFunctionLines: Number.isFinite(args.minFunctionLines)
|
||||
? Number(args.minFunctionLines)
|
||||
: DEFAULT_MIN_FUNCTION_LINES,
|
||||
minVariableLines: Number.isFinite(args.minVariableLines)
|
||||
? Number(args.minVariableLines)
|
||||
: DEFAULT_MIN_VARIABLE_LINES,
|
||||
utilNamePattern: new RegExp(args.utilPattern ?? DEFAULT_UTIL_PATTERN),
|
||||
};
|
||||
|
||||
const refactorer = new TSXRefactorer(options);
|
||||
|
||||
// Step 1: Analyze
|
||||
const candidates = refactorer.analyzeFile();
|
||||
|
||||
Reference in New Issue
Block a user