mirror of
https://github.com/johndoe6345789/tsmorph.git
synced 2026-04-24 13:54:58 +00:00
461 lines
12 KiB
Markdown
461 lines
12 KiB
Markdown
# TSX Refactoring with ts-morph
|
|
|
|
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
|
|
|
|
✨ **Automated Code Extraction**
|
|
- 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
|
|
- Adds parameter type annotations
|
|
- Replaces `any` types with specific types
|
|
- Reports type coverage metrics
|
|
|
|
🎨 **Lint Auto-Fixing**
|
|
- Integrates with ESLint and Prettier
|
|
- Organizes imports automatically
|
|
- Fixes common linting issues
|
|
- Ensures code style consistency
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
tsmorph/
|
|
├── src/
|
|
│ └── components/
|
|
│ ├── UserManagementDashboard.tsx # Main component (532 LOC)
|
|
│ ├── UserManagementDashboard.types.ts # Extracted types (22 LOC)
|
|
│ └── UserManagementDashboard.utils.ts # Extracted utilities (50 LOC)
|
|
├── scripts/
|
|
│ ├── refactor-tsx.ts # Main refactoring script (pass 1)
|
|
│ ├── refactor-tsx-pass2.ts # Extract inner functions (pass 2)
|
|
│ ├── analyze-types.ts # Type analysis and auto-fixing
|
|
│ └── fix-lint.ts # Lint auto-fixer
|
|
├── package.json
|
|
├── tsconfig.json
|
|
├── .eslintrc.js
|
|
└── .prettierrc
|
|
```
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install
|
|
```
|
|
|
|
## Usage
|
|
|
|
### Quick Start - Full Refactoring Pipeline
|
|
|
|
```bash
|
|
# Step 1: Extract types and interfaces
|
|
npm run refactor
|
|
|
|
# Step 2: Extract utility functions
|
|
npm run refactor:pass2
|
|
|
|
# Step 3: Add missing types and improve type annotations
|
|
npm run analyze-types
|
|
|
|
# Step 4: Fix linting issues and format code
|
|
npm run fix-lint
|
|
|
|
# Step 5: Verify everything compiles
|
|
npm run type-check
|
|
```
|
|
|
|
### Individual Commands
|
|
|
|
| Command | Description |
|
|
|---------|-------------|
|
|
| `npm run refactor` | Extract types/interfaces from component |
|
|
| `npm run refactor:pass2` | Extract utility functions from component |
|
|
| `npm run analyze-types` | Analyze and fix type annotations |
|
|
| `npm run fix-lint` | Auto-fix lint issues and format code |
|
|
| `npm run type-check` | Verify TypeScript compilation |
|
|
| `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
|
|
|
|
```tsx
|
|
// UserManagementDashboard.tsx - Everything in one file
|
|
import React, { useState } from 'react';
|
|
|
|
interface User {
|
|
id: string;
|
|
name: string;
|
|
// ... more fields
|
|
}
|
|
|
|
interface FormData {
|
|
// ...
|
|
}
|
|
|
|
export const UserManagementDashboard: React.FC = () => {
|
|
const validateForm = (data: FormData) => {
|
|
// validation logic...
|
|
};
|
|
|
|
const getRoleBadgeColor = (role: string) => {
|
|
// helper logic...
|
|
};
|
|
|
|
// ... 500+ more lines
|
|
};
|
|
```
|
|
|
|
### After: Refactored & Organized
|
|
|
|
**UserManagementDashboard.types.ts** (22 LOC)
|
|
```typescript
|
|
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;
|
|
}
|
|
```
|
|
|
|
**UserManagementDashboard.utils.ts** (50 LOC)
|
|
```typescript
|
|
import type { FormData, ValidationErrors } from './UserManagementDashboard.types';
|
|
|
|
export const validateForm = (data: FormData): ValidationErrors => {
|
|
const errors: ValidationErrors = {};
|
|
// validation logic...
|
|
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',
|
|
});
|
|
};
|
|
```
|
|
|
|
**UserManagementDashboard.tsx** (size varies)
|
|
```tsx
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { User, FormData, ValidationErrors } from './UserManagementDashboard.types';
|
|
import {
|
|
validateForm,
|
|
getRoleBadgeColor,
|
|
getStatusBadgeColor,
|
|
formatDate
|
|
} from './UserManagementDashboard.utils';
|
|
|
|
export const UserManagementDashboard: React.FC = (): React.JSX.Element => {
|
|
// Component logic...
|
|
};
|
|
```
|
|
|
|
## How It Works
|
|
|
|
### 1. Type Extraction (`refactor-tsx.ts`)
|
|
|
|
The script analyzes the AST using ts-morph to:
|
|
- Find all interface and type alias declarations
|
|
- Extract them to a separate `.types.ts` file
|
|
- Add `export` keywords automatically
|
|
- Remove them from the original file
|
|
- Add import statement to the original file
|
|
|
|
```typescript
|
|
// Analyzes file structure
|
|
const interfaces = sourceFile.getInterfaces();
|
|
const typeAliases = sourceFile.getTypeAliases();
|
|
|
|
// Extracts to new file with exports
|
|
typesFile.replaceWithText(typesContent);
|
|
|
|
// Adds import to original
|
|
sourceFile.addImportDeclaration({
|
|
moduleSpecifier: './Component.types',
|
|
namedImports: typeNames,
|
|
});
|
|
```
|
|
|
|
### 2. Utility Function Extraction (`refactor-tsx-pass2.ts`)
|
|
|
|
Identifies and extracts helper functions:
|
|
- Finds const declarations with arrow functions inside components
|
|
- Filters by naming patterns (`validate*`, `get*`, `format*`, `handle*`)
|
|
- Exports them with proper type imports
|
|
- Updates original component with imports
|
|
|
|
```typescript
|
|
// Find helper functions in component body
|
|
const helperFunctions = statements
|
|
.filter(stmt => stmt.isKind(SyntaxKind.VariableStatement))
|
|
.filter(decl => decl.name.match(/^(validate|get|format|handle)/));
|
|
|
|
// Extract and make exported
|
|
const exportedText = text.replace(/^(const|let|var)/, 'export const');
|
|
```
|
|
|
|
### 3. Type Analysis (`analyze-types.ts`)
|
|
|
|
Automatically improves type safety:
|
|
- **Adds return types**: Infers from function body
|
|
- **Adds parameter types**: Uses TypeScript's type inference
|
|
- **Fixes 'any' types**: Replaces with specific inferred types
|
|
- **Ensures exports**: Adds missing `export` keywords to types
|
|
- **Type coverage report**: Shows typed vs untyped declarations
|
|
|
|
```typescript
|
|
// Infer and add return type
|
|
const returnType = func.getReturnType();
|
|
const typeText = returnType.getText(func);
|
|
func.setReturnType(typeText);
|
|
|
|
// Example output:
|
|
// ✓ Added return type to getRoleBadgeColor: "#ff6b6b" | "#4ecdc4" | "#95a5a6" | "#7f8c8d"
|
|
```
|
|
|
|
### 4. Lint Auto-Fixing (`fix-lint.ts`)
|
|
|
|
Ensures code quality:
|
|
- **Organize imports**: Sorts React first, then external, then local
|
|
- **Format with Prettier**: Consistent code style
|
|
- **Fix with ESLint**: Auto-fixes common issues
|
|
- **Non-destructive**: Only applies safe transformations
|
|
|
|
## Type Analysis Output Example
|
|
|
|
```
|
|
📊 Type Analysis: UserManagementDashboard.utils.ts
|
|
✓ Added return type to validateForm: ValidationErrors
|
|
✓ Added return type to getRoleBadgeColor: "#ff6b6b" | "#4ecdc4" | "#95a5a6" | "#7f8c8d"
|
|
✓ Added return type to getStatusBadgeColor: "#2ecc71" | "#e74c3c"
|
|
✓ Added return type to formatDate: string
|
|
💾 Saved 4 type improvements
|
|
|
|
📈 Type Coverage Report
|
|
========================
|
|
UserManagementDashboard.utils.ts:
|
|
Type Coverage: 100% (4/4 declarations typed)
|
|
✅ No 'any' types found
|
|
```
|
|
|
|
## Configuration
|
|
|
|
### TypeScript Configuration (`tsconfig.json`)
|
|
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"strict": true,
|
|
"noImplicitAny": true,
|
|
"esModuleInterop": true,
|
|
"jsx": "preserve",
|
|
"moduleResolution": "bundler"
|
|
}
|
|
}
|
|
```
|
|
|
|
### ESLint Configuration (`.eslintrc.js`)
|
|
|
|
```javascript
|
|
module.exports = {
|
|
parser: '@typescript-eslint/parser',
|
|
extends: [
|
|
'eslint:recommended',
|
|
'plugin:@typescript-eslint/recommended',
|
|
'prettier',
|
|
],
|
|
rules: {
|
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
|
'@typescript-eslint/no-explicit-any': 'warn',
|
|
},
|
|
};
|
|
```
|
|
|
|
### Prettier Configuration (`.prettierrc`)
|
|
|
|
```json
|
|
{
|
|
"semi": true,
|
|
"singleQuote": true,
|
|
"printWidth": 100,
|
|
"trailingComma": "es5"
|
|
}
|
|
```
|
|
|
|
## Benefits
|
|
|
|
### ✅ Maintainability
|
|
- **Smaller files**: Each file has a single responsibility
|
|
- **Easy to locate**: Types and utilities are in predictable locations
|
|
- **Reduced complexity**: Component focuses on UI logic
|
|
|
|
### ✅ Reusability
|
|
- **Shared types**: Can be imported across multiple components
|
|
- **Utility functions**: Easily tested and reused
|
|
- **DRY principle**: Eliminates code duplication
|
|
|
|
### ✅ Type Safety
|
|
- **100% type coverage**: All functions have proper type annotations
|
|
- **Precise types**: Union types for string literals (e.g., color values)
|
|
- **No 'any' types**: Strong typing throughout
|
|
|
|
### ✅ Code Quality
|
|
- **Lint-compliant**: All code passes ESLint checks
|
|
- **Consistent style**: Prettier ensures uniform formatting
|
|
- **Auto-formatted**: No manual formatting needed
|
|
|
|
## Metrics
|
|
|
|
| Metric | Before | After | Improvement |
|
|
|--------|--------|-------|-------------|
|
|
| Main component LOC | 603 | 532 | -12% |
|
|
| Files | 1 | 3 | Organized |
|
|
| Type coverage | ~60% | 100% | +40% |
|
|
| 'any' types | Several | 0 | Eliminated |
|
|
| Imports managed | Manual | Automatic | Automated |
|
|
|
|
## Advanced Usage
|
|
|
|
### Customizing Extraction Patterns
|
|
|
|
Edit `scripts/refactor-tsx-pass2.ts` to change which functions get extracted:
|
|
|
|
```typescript
|
|
// Current pattern
|
|
if (name.match(/^(validate|getRoleBadgeColor|getStatusBadgeColor|formatDate)/)) {
|
|
helperFunctions.push({ name, text });
|
|
}
|
|
|
|
// Custom pattern - extract all 'use*' hooks
|
|
if (name.match(/^use[A-Z]/)) {
|
|
helperFunctions.push({ name, text });
|
|
}
|
|
```
|
|
|
|
### Running on Different Files
|
|
|
|
Modify the file paths in each script:
|
|
|
|
```typescript
|
|
const targetFile = path.join(
|
|
__dirname,
|
|
'..',
|
|
'src',
|
|
'components',
|
|
'YourComponent.tsx' // Change this
|
|
);
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
1. **Run in sequence**: Execute refactor → refactor:pass2 → analyze-types → fix-lint
|
|
2. **Review changes**: Always review extracted code before committing
|
|
3. **Test thoroughly**: Run type-check after each refactoring step
|
|
4. **Commit incrementally**: Commit after each successful extraction
|
|
5. **Keep naming consistent**: Use predictable naming patterns for utilities
|
|
|
|
## Troubleshooting
|
|
|
|
### "Cannot find name" errors after refactoring
|
|
|
|
**Problem**: Imports were not added correctly.
|
|
|
|
**Solution**:
|
|
```bash
|
|
# Re-run the refactoring scripts
|
|
npm run refactor
|
|
npm run refactor:pass2
|
|
npm run type-check
|
|
```
|
|
|
|
### Type inference produces complex types
|
|
|
|
**Problem**: Auto-generated types are too long or complex.
|
|
|
|
**Solution**: Manually simplify the type or add explicit annotations before refactoring.
|
|
|
|
### Lint issues remain after fix-lint
|
|
|
|
**Problem**: Some lint rules require manual fixes.
|
|
|
|
**Solution**:
|
|
```bash
|
|
npm run lint # Review remaining issues
|
|
npm run lint:fix # Try auto-fix again
|
|
```
|
|
|
|
## Contributing
|
|
|
|
To add new refactoring patterns:
|
|
|
|
1. Create a new script in `scripts/`
|
|
2. Use ts-morph to analyze and transform the AST
|
|
3. Add a corresponding npm script in `package.json`
|
|
4. Update this README with usage instructions
|
|
|
|
## Dependencies
|
|
|
|
- **ts-morph** (^21.0.0): TypeScript compiler API wrapper
|
|
- **typescript** (^5.0.0): TypeScript compiler
|
|
- **eslint** (^8.0.0): JavaScript/TypeScript linter
|
|
- **prettier** (^3.0.0): Code formatter
|
|
- **ts-node** (^10.9.0): Execute TypeScript directly
|
|
|
|
## License
|
|
|
|
MIT
|
|
|
|
## Resources
|
|
|
|
- [ts-morph Documentation](https://ts-morph.com/)
|
|
- [TypeScript Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API)
|
|
- [ESLint Rules](https://eslint.org/docs/rules/)
|
|
- [Prettier Options](https://prettier.io/docs/en/options.html)
|