mirror of https://github.com/langgenius/dify.git
416 lines
16 KiB
JavaScript
416 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from 'node:child_process'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import {
|
|
ComponentAnalyzer,
|
|
extractCopyContent,
|
|
getComplexityLevel,
|
|
listAnalyzableFiles,
|
|
resolveDirectoryEntry,
|
|
} from './component-analyzer.js'
|
|
|
|
// ============================================================================
|
|
// Extended Analyzer for Refactoring
|
|
// ============================================================================
|
|
|
|
class RefactorAnalyzer extends ComponentAnalyzer {
|
|
analyze(code, filePath, absolutePath) {
|
|
// Get base analysis from parent class
|
|
const baseAnalysis = super.analyze(code, filePath, absolutePath)
|
|
|
|
// Add refactoring-specific metrics
|
|
// Note: These counts use regex matching which may include import statements.
|
|
// For most components this results in +1 over actual usage, which is acceptable
|
|
// for heuristic analysis. For precise AST-based counting, consider using
|
|
// @typescript-eslint/parser to traverse the AST.
|
|
const stateCount = (code.match(/useState\s*[(<]/g) || []).length
|
|
const effectCount = (code.match(/useEffect\s*\(/g) || []).length
|
|
const callbackCount = (code.match(/useCallback\s*\(/g) || []).length
|
|
const memoCount = (code.match(/useMemo\s*\(/g) || []).length
|
|
const conditionalBlocks = this.countConditionalBlocks(code)
|
|
const nestedTernaries = this.countNestedTernaries(code)
|
|
const hasContext = code.includes('useContext') || code.includes('createContext')
|
|
const hasReducer = code.includes('useReducer')
|
|
const hasModals = this.countModals(code)
|
|
|
|
return {
|
|
...baseAnalysis,
|
|
stateCount,
|
|
effectCount,
|
|
callbackCount,
|
|
memoCount,
|
|
conditionalBlocks,
|
|
nestedTernaries,
|
|
hasContext,
|
|
hasReducer,
|
|
hasModals,
|
|
}
|
|
}
|
|
|
|
countModals(code) {
|
|
const modalPatterns = [
|
|
/Modal/g,
|
|
/Dialog/g,
|
|
/Drawer/g,
|
|
/Confirm/g,
|
|
/showModal|setShowModal|isShown|isShowing/g,
|
|
]
|
|
let count = 0
|
|
modalPatterns.forEach((pattern) => {
|
|
const matches = code.match(pattern)
|
|
if (matches)
|
|
count += matches.length
|
|
})
|
|
return Math.floor(count / 3) // Rough estimate of actual modals
|
|
}
|
|
|
|
countConditionalBlocks(code) {
|
|
const ifBlocks = (code.match(/\bif\s*\(/g) || []).length
|
|
const ternaries = (code.match(/\?.*:/g) || []).length
|
|
const switchCases = (code.match(/\bswitch\s*\(/g) || []).length
|
|
return ifBlocks + ternaries + switchCases
|
|
}
|
|
|
|
countNestedTernaries(code) {
|
|
const nestedInTrueBranch = (code.match(/\?[^:?]*\?[^:]*:/g) || []).length
|
|
const nestedInFalseBranch = (code.match(/\?[^:?]*:[^?]*\?[^:]*:/g) || []).length
|
|
|
|
return nestedInTrueBranch + nestedInFalseBranch
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Refactor Prompt Builder
|
|
// ============================================================================
|
|
|
|
class RefactorPromptBuilder {
|
|
build(analysis) {
|
|
const refactorActions = this.identifyRefactorActions(analysis)
|
|
|
|
return `
|
|
╔════════════════════════════════════════════════════════════════════════════╗
|
|
║ 🔧 REFACTOR DIFY COMPONENT ║
|
|
╚════════════════════════════════════════════════════════════════════════════╝
|
|
|
|
📍 Component: ${analysis.name}
|
|
📂 Path: ${analysis.path}
|
|
|
|
📊 Complexity Analysis:
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
Total Complexity: ${analysis.complexity}/100 ${getComplexityLevel(analysis.complexity)}
|
|
Max Func Complexity: ${analysis.maxComplexity}/100 ${getComplexityLevel(analysis.maxComplexity)}
|
|
Lines: ${analysis.lineCount} ${analysis.lineCount > 300 ? '⚠️ TOO LARGE' : ''}
|
|
Usage: ${analysis.usageCount} reference${analysis.usageCount !== 1 ? 's' : ''}
|
|
|
|
📈 Code Metrics:
|
|
useState calls: ${analysis.stateCount}
|
|
useEffect calls: ${analysis.effectCount}
|
|
useCallback calls: ${analysis.callbackCount}
|
|
useMemo calls: ${analysis.memoCount}
|
|
Conditional blocks: ${analysis.conditionalBlocks}
|
|
Nested ternaries: ${analysis.nestedTernaries}
|
|
Modal components: ${analysis.hasModals}
|
|
|
|
🔍 Features Detected:
|
|
${analysis.hasState ? '✓' : '✗'} Local state (useState/useReducer)
|
|
${analysis.hasEffects ? '✓' : '✗'} Side effects (useEffect)
|
|
${analysis.hasCallbacks ? '✓' : '✗'} Callbacks (useCallback)
|
|
${analysis.hasMemo ? '✓' : '✗'} Memoization (useMemo)
|
|
${analysis.hasContext ? '✓' : '✗'} Context (useContext/createContext)
|
|
${analysis.hasEvents ? '✓' : '✗'} Event handlers
|
|
${analysis.hasRouter ? '✓' : '✗'} Next.js routing
|
|
${analysis.hasAPI ? '✓' : '✗'} API calls
|
|
${analysis.hasReactQuery ? '✓' : '✗'} React Query
|
|
${analysis.hasAhooks ? '✓' : '✗'} ahooks
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
🎯 RECOMMENDED REFACTORING ACTIONS:
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
${refactorActions.map((action, i) => `${i + 1}. ${action}`).join('\n')}
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
📋 PROMPT FOR AI ASSISTANT (COPY THIS TO YOUR AI ASSISTANT):
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
Please refactor the component at @${analysis.path}
|
|
|
|
Component metrics:
|
|
- Complexity: ${analysis.complexity}/100 (target: < 50)
|
|
- Lines: ${analysis.lineCount} (target: < 300)
|
|
- useState: ${analysis.stateCount}, useEffect: ${analysis.effectCount}
|
|
|
|
Refactoring tasks:
|
|
${refactorActions.map(action => `- ${action}`).join('\n')}
|
|
|
|
Requirements:
|
|
${this.buildRequirements(analysis)}
|
|
|
|
Follow Dify project conventions:
|
|
- Place extracted hooks in \`hooks/\` subdirectory or as \`use-<feature>.ts\`
|
|
- Use React Query (\`@tanstack/react-query\`) for data fetching
|
|
- Follow existing patterns in \`web/service/use-*.ts\` for API hooks
|
|
- Keep each new file under 300 lines
|
|
- Maintain TypeScript strict typing
|
|
|
|
After refactoring, verify:
|
|
- \`pnpm lint:fix\` passes
|
|
- \`pnpm type-check:tsgo\` passes
|
|
- Re-run \`pnpm refactor-component ${analysis.path}\` to confirm complexity < 50
|
|
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
`
|
|
}
|
|
|
|
identifyRefactorActions(analysis) {
|
|
const actions = []
|
|
|
|
// Priority 1: Extract hooks for complex state management
|
|
if (analysis.stateCount >= 3 || (analysis.stateCount >= 2 && analysis.effectCount >= 2)) {
|
|
actions.push(`🪝 EXTRACT CUSTOM HOOK: ${analysis.stateCount} useState + ${analysis.effectCount} useEffect detected. Extract related state and effects into a custom hook (e.g., \`use${analysis.name}State.ts\`)`)
|
|
}
|
|
|
|
// Priority 2: Extract API/data logic
|
|
if (analysis.hasAPI)
|
|
actions.push('🌐 EXTRACT DATA HOOK: Move API calls and data fetching logic into a dedicated hook using React Query')
|
|
|
|
// Priority 3: Split large components
|
|
if (analysis.lineCount > 300) {
|
|
actions.push(`📦 SPLIT COMPONENT: ${analysis.lineCount} lines exceeds limit. Extract UI sections into sub-components`)
|
|
}
|
|
|
|
// Priority 4: Extract modal management
|
|
if (analysis.hasModals >= 2) {
|
|
actions.push(`🔲 EXTRACT MODAL MANAGEMENT: ${analysis.hasModals} modal-related patterns detected. Create a useModalState hook or separate modal components`)
|
|
}
|
|
|
|
// Priority 5: Simplify conditionals
|
|
if (analysis.conditionalBlocks > 10 || analysis.nestedTernaries >= 2) {
|
|
actions.push('🔀 SIMPLIFY CONDITIONALS: Use lookup tables, early returns, or extract complex conditions into named functions')
|
|
}
|
|
|
|
// Priority 6: Extract callbacks
|
|
if (analysis.callbackCount >= 4) {
|
|
actions.push(`⚡ CONSOLIDATE CALLBACKS: ${analysis.callbackCount} useCallback calls. Consider extracting related callbacks into a custom hook`)
|
|
}
|
|
|
|
// Priority 7: Context provider extraction
|
|
if (analysis.hasContext && analysis.complexity > 50) {
|
|
actions.push('🎯 EXTRACT CONTEXT LOGIC: Move context provider logic into separate files or split into domain-specific contexts')
|
|
}
|
|
|
|
// Priority 8: Memoization review
|
|
if (analysis.memoCount >= 3 && analysis.complexity > 50) {
|
|
actions.push(`📝 REVIEW MEMOIZATION: ${analysis.memoCount} useMemo calls. Extract complex computations into utility functions or hooks`)
|
|
}
|
|
|
|
// If no specific issues, provide general guidance
|
|
if (actions.length === 0) {
|
|
if (analysis.complexity > 50) {
|
|
actions.push('🔍 ANALYZE FUNCTIONS: Review individual functions for complexity and extract helper functions')
|
|
}
|
|
else {
|
|
actions.push('✅ Component complexity is acceptable. Consider minor improvements for maintainability')
|
|
}
|
|
}
|
|
|
|
return actions
|
|
}
|
|
|
|
buildRequirements(analysis) {
|
|
const requirements = []
|
|
|
|
if (analysis.stateCount >= 3) {
|
|
requirements.push('- Group related useState calls into a single custom hook')
|
|
requirements.push('- Move associated useEffect calls with the state they depend on')
|
|
}
|
|
|
|
if (analysis.hasAPI) {
|
|
requirements.push('- Create data fetching hook following web/service/use-*.ts patterns')
|
|
requirements.push('- Use useQuery with proper queryKey and enabled options')
|
|
requirements.push('- Export invalidation hook (useInvalidXxx) for cache management')
|
|
}
|
|
|
|
if (analysis.lineCount > 300) {
|
|
requirements.push('- Extract logical UI sections into separate components')
|
|
requirements.push('- Keep parent component focused on orchestration')
|
|
requirements.push('- Pass minimal props to child components')
|
|
}
|
|
|
|
if (analysis.hasModals >= 2) {
|
|
requirements.push('- Create unified modal state management')
|
|
requirements.push('- Consider extracting modals to separate file')
|
|
}
|
|
|
|
if (analysis.conditionalBlocks > 10) {
|
|
requirements.push('- Replace switch statements with lookup tables')
|
|
requirements.push('- Use early returns to reduce nesting')
|
|
requirements.push('- Extract complex boolean logic to named functions')
|
|
}
|
|
|
|
if (requirements.length === 0) {
|
|
requirements.push('- Maintain existing code structure')
|
|
requirements.push('- Focus on readability improvements')
|
|
}
|
|
|
|
return requirements.join('\n')
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main Function
|
|
// ============================================================================
|
|
|
|
function showHelp() {
|
|
console.log(`
|
|
🔧 Component Refactor Tool - Generate refactoring prompts for AI assistants
|
|
|
|
Usage:
|
|
node refactor-component.js <component-path> [options]
|
|
pnpm refactor-component <component-path> [options]
|
|
|
|
Options:
|
|
--help Show this help message
|
|
--json Output analysis result as JSON (for programmatic use)
|
|
|
|
Examples:
|
|
# Analyze and generate refactoring prompt
|
|
pnpm refactor-component app/components/app/configuration/index.tsx
|
|
|
|
# Output as JSON
|
|
pnpm refactor-component app/components/tools/mcp/modal.tsx --json
|
|
|
|
Complexity Thresholds:
|
|
🟢 0-25: Simple (no refactoring needed)
|
|
🟡 26-50: Medium (consider minor refactoring)
|
|
🟠 51-75: Complex (should refactor)
|
|
🔴 76-100: Very Complex (must refactor)
|
|
|
|
For complete refactoring guidelines, see:
|
|
.claude/skills/component-refactoring/SKILL.md
|
|
`)
|
|
}
|
|
|
|
function main() {
|
|
const rawArgs = process.argv.slice(2)
|
|
|
|
let isJsonMode = false
|
|
const args = []
|
|
|
|
rawArgs.forEach((arg) => {
|
|
if (arg === '--json') {
|
|
isJsonMode = true
|
|
return
|
|
}
|
|
if (arg === '--help' || arg === '-h') {
|
|
showHelp()
|
|
process.exit(0)
|
|
}
|
|
args.push(arg)
|
|
})
|
|
|
|
if (args.length === 0) {
|
|
showHelp()
|
|
process.exit(1)
|
|
}
|
|
|
|
let componentPath = args[0]
|
|
let absolutePath = path.resolve(process.cwd(), componentPath)
|
|
|
|
if (!fs.existsSync(absolutePath)) {
|
|
console.error(`❌ Error: Path not found: ${componentPath}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
if (fs.statSync(absolutePath).isDirectory()) {
|
|
const resolvedFile = resolveDirectoryEntry(absolutePath, componentPath)
|
|
if (resolvedFile) {
|
|
absolutePath = resolvedFile.absolutePath
|
|
componentPath = resolvedFile.componentPath
|
|
}
|
|
else {
|
|
const availableFiles = listAnalyzableFiles(absolutePath)
|
|
console.error(`❌ Error: Directory does not contain a recognizable entry file: ${componentPath}`)
|
|
if (availableFiles.length > 0) {
|
|
console.error(`\n Available files to analyze:`)
|
|
availableFiles.forEach(f => console.error(` - ${path.join(componentPath, f)}`))
|
|
console.error(`\n Please specify the exact file path, e.g.:`)
|
|
console.error(` pnpm refactor-component ${path.join(componentPath, availableFiles[0])}`)
|
|
}
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
const sourceCode = fs.readFileSync(absolutePath, 'utf-8')
|
|
|
|
const analyzer = new RefactorAnalyzer()
|
|
const analysis = analyzer.analyze(sourceCode, componentPath, absolutePath)
|
|
|
|
// JSON output mode
|
|
if (isJsonMode) {
|
|
console.log(JSON.stringify(analysis, null, 2))
|
|
return
|
|
}
|
|
|
|
// Check if refactoring is needed
|
|
if (analysis.complexity <= 25 && analysis.lineCount <= 200) {
|
|
console.log(`
|
|
╔════════════════════════════════════════════════════════════════════════════╗
|
|
║ ✅ COMPONENT IS WELL-STRUCTURED ║
|
|
╚════════════════════════════════════════════════════════════════════════════╝
|
|
|
|
📍 Component: ${analysis.name}
|
|
📂 Path: ${analysis.path}
|
|
|
|
📊 Metrics:
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
Complexity: ${analysis.complexity}/100 🟢 Simple
|
|
Lines: ${analysis.lineCount} ✓ Within limits
|
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
|
|
This component has good structure. No immediate refactoring needed.
|
|
You can proceed with testing using: pnpm analyze-component ${componentPath}
|
|
`)
|
|
return
|
|
}
|
|
|
|
// Build refactoring prompt
|
|
const builder = new RefactorPromptBuilder()
|
|
const prompt = builder.build(analysis)
|
|
|
|
console.log(prompt)
|
|
|
|
// Copy to clipboard (macOS)
|
|
try {
|
|
const checkPbcopy = spawnSync('which', ['pbcopy'], { stdio: 'pipe' })
|
|
if (checkPbcopy.status !== 0)
|
|
return
|
|
const copyContent = extractCopyContent(prompt)
|
|
if (!copyContent)
|
|
return
|
|
|
|
const result = spawnSync('pbcopy', [], {
|
|
input: copyContent,
|
|
encoding: 'utf-8',
|
|
})
|
|
|
|
if (result.status === 0) {
|
|
console.log('\n📋 Refactoring prompt copied to clipboard!')
|
|
console.log(' Paste it in your AI assistant:')
|
|
console.log(' - Cursor: Cmd+L (Chat) or Cmd+I (Composer)')
|
|
console.log(' - GitHub Copilot Chat: Cmd+I')
|
|
console.log(' - Or any other AI coding tool\n')
|
|
}
|
|
}
|
|
catch {
|
|
// pbcopy failed, but don't break the script
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Run
|
|
// ============================================================================
|
|
|
|
main()
|