UNPKG

@tachui/cli

Version:

Tacho CLI - Comprehensive developer tooling for tachUI

550 lines 27.7 kB
/** * Tacho CLI - Analyze Command * * Code analysis and performance insights for TachUI applications */ import { readFileSync, statSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import * as ts from 'typescript'; import chalk from 'chalk'; import { Command } from 'commander'; import { glob } from 'glob'; import ora from 'ora'; /** * CLI-specific concatenation pattern analysis using TypeScript AST * Replaces regex-based approach with proper AST analysis */ function analyzeConcatenationPatterns(content, filename = 'temp.ts') { const patterns = []; try { // Parse the code into a TypeScript AST const sourceFile = ts.createSourceFile(filename, content, ts.ScriptTarget.Latest, true, filename.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS); // Walk the AST to find .concat() calls function visit(node) { if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'concat') { // Check if this is a .build().concat() pattern const leftExpression = node.expression.expression; if (ts.isCallExpression(leftExpression) && ts.isPropertyAccessExpression(leftExpression.expression) && leftExpression.expression.name.text === 'build') { // Extract left component (everything before .build()) const componentChain = leftExpression.expression.expression; const leftComponent = extractComponentExpression(componentChain, content); // Extract right component (first argument to .concat()) const rightComponent = node.arguments.length > 0 ? extractComponentExpression(node.arguments[0], content) : ''; if (leftComponent && rightComponent) { // Get source location const start = node.getStart(sourceFile); const end = node.getEnd(); // Simple static analysis const hasVariable = /\$\{[^}]+\}|[a-zA-Z_$][a-zA-Z0-9_$]*\[[^\]]*\]|entry\[/.test(leftComponent + rightComponent); const isStatic = !hasVariable; // Accessibility analysis const hasInteractive = /\bButton\b|\bLink\b/i.test(leftComponent + rightComponent); const hasComplexLayout = /\bVStack\b|\bHStack\b|\bZStack\b/i.test(content); let accessibilityNeeds; if (hasComplexLayout) { accessibilityNeeds = 'full'; } else if (hasInteractive) { accessibilityNeeds = 'aria'; } else { accessibilityNeeds = 'minimal'; } patterns.push({ type: isStatic ? 'static' : 'dynamic', location: { start, end }, leftComponent: leftComponent.trim(), rightComponent: rightComponent.trim(), optimizable: isStatic, accessibilityNeeds, }); } } } // Continue traversing child nodes ts.forEachChild(node, visit); } visit(sourceFile); } catch (error) { // If AST parsing fails, return empty patterns (graceful fallback) console.warn(`Failed to parse for concatenation analysis:`, error); } return patterns; } /** * Helper function to extract component expression text from AST node */ function extractComponentExpression(node, sourceCode) { const start = node.getStart(); const end = node.getEnd(); return sourceCode.substring(start, end); } /** * Generate comprehensive concatenation analysis for CLI reporting */ function generateConcatenationAnalysis(allPatterns) { const staticPatterns = allPatterns.filter(p => p.type === 'static').length; const dynamicPatterns = allPatterns.filter(p => p.type === 'dynamic').length; const optimizedPatterns = allPatterns.filter(p => p.optimizable).length; // Calculate bundle savings const baseConcatenationSize = 87.76; // KB const selectedRuntimes = new Set(allPatterns.map(p => p.accessibilityNeeds)); const optimizedSize = selectedRuntimes.size * 5; // 5KB per runtime const bundleSavingsKB = Math.max(0, baseConcatenationSize - optimizedSize); // Accessibility breakdown const accessibilityBreakdown = { minimal: allPatterns.filter(p => p.accessibilityNeeds === 'minimal').length, aria: allPatterns.filter(p => p.accessibilityNeeds === 'aria').length, full: allPatterns.filter(p => p.accessibilityNeeds === 'full').length, }; // Generate recommendations const recommendations = []; if (dynamicPatterns > staticPatterns) { recommendations.push('Consider extracting variables from concatenation patterns to enable static optimization'); } if (accessibilityBreakdown.full > accessibilityBreakdown.minimal + accessibilityBreakdown.aria) { recommendations.push('Many concatenations require full accessibility - consider simplifying component structures'); } if (allPatterns.length > 10 && optimizedPatterns / allPatterns.length < 0.5) { recommendations.push('Low optimization rate - review concatenation patterns for static optimization opportunities'); } if (bundleSavingsKB > 50) { recommendations.push(`High bundle savings potential: ${bundleSavingsKB.toFixed(1)}KB - implement concatenation optimization`); } return { totalPatterns: allPatterns.length, optimizedPatterns, staticPatterns, dynamicPatterns, bundleSavingsKB, accessibilityBreakdown, recommendations, }; } function analyzeFile(filePath, content) { const lines = content.split('\n'); const analysis = { file: filePath, lines: lines.length, size: Buffer.byteLength(content, 'utf8'), hasState: false, hasModifiers: false, hasLifecycle: false, hasNavigation: false, imports: [], exports: [], components: [], stateUsage: [], modifierUsage: [], layoutUsage: [], lifecycleUsage: [], navigationUsage: [], complexity: 0, concatenationPatterns: [], }; // Analyze imports const importMatches = content.match(/import.*from ['"]@tachui\/core['"]/g) || []; analysis.imports = importMatches; // Analyze state usage if (content.includes('State(') || content.includes('ObservedObject(') || content.includes('EnvironmentObject(')) { analysis.hasState = true; const stateMatches = content.match(/const \w+ = State\([^)]*\)/g) || []; analysis.stateUsage.push(...stateMatches); } // Analyze modifier usage if (content.includes('.modifier')) { analysis.hasModifiers = true; const modifierChains = content.match(/\.modifier[\s\S]*?\.build\(\)/g) || []; analysis.modifierUsage = modifierChains.map(chain => chain .split('.') .filter(part => part.trim() && !part.includes('modifier') && !part.includes('build()')) .join('.')); } // Analyze layout usage const layoutMatches = content.match(/Layout\.(VStack|HStack|ZStack)/g) || []; analysis.layoutUsage = [...new Set(layoutMatches)]; if (layoutMatches.length > 0) { analysis.complexity += layoutMatches.length; } // Analyze lifecycle usage const lifecycleModifiers = ['onAppear', 'onDisappear', 'task', 'refreshable']; lifecycleModifiers.forEach(modifier => { if (content.includes(`.${modifier}(`)) { analysis.hasLifecycle = true; analysis.lifecycleUsage.push(modifier); } }); // Analyze navigation usage const navigationComponents = [ 'NavigationView', 'NavigationLink', 'TabView', 'useNavigation', ]; navigationComponents.forEach(component => { if (content.includes(component)) { analysis.hasNavigation = true; analysis.navigationUsage.push(component); } }); // Find component definitions const componentMatches = content.match(/export function (\w+)\(/g) || []; analysis.components = componentMatches.map(match => match.replace('export function ', '').replace('(', '')); // Calculate complexity score analysis.complexity += (content.match(/if\s*\(/g) || []).length * 2; analysis.complexity += (content.match(/for\s*\(/g) || []).length * 2; analysis.complexity += (content.match(/while\s*\(/g) || []).length * 2; analysis.complexity += (content.match(/switch\s*\(/g) || []).length * 3; analysis.complexity += (content.match(/\.map\(/g) || []).length; analysis.complexity += (content.match(/\.filter\(/g) || []).length; analysis.complexity += analysis.modifierUsage.length * 0.5; // Analyze concatenation patterns analysis.concatenationPatterns = analyzeConcatenationPatterns(content, filePath); return analysis; } function generateSuggestions(results) { const suggestions = []; // State management suggestions if (results.components.withState < results.components.total * 0.3) { suggestions.push('Consider using @State for more reactive components'); } // Modifier usage suggestions if (results.components.withModifiers < results.components.total * 0.7) { suggestions.push('Use TachUI modifiers for consistent styling (.modifier.padding(), .foregroundColor())'); } // Lifecycle suggestions if (results.components.withLifecycle < results.components.total * 0.2) { suggestions.push('Consider using lifecycle modifiers (onAppear, task) for better component behavior'); } // Performance suggestions if (results.performance.largeComponents.length > 0) { suggestions.push(`Break down large components (${results.performance.largeComponents.length} components >200 lines)`); } if (results.performance.complexComponents.length > 0) { suggestions.push(`Simplify complex components (${results.performance.complexComponents.length} components with high complexity)`); } // Pattern suggestions const hasNavigation = results.patterns.navigationUsage.length > 0; if (!hasNavigation && results.components.total > 3) { suggestions.push('Consider adding navigation (NavigationView, TabView) for better user experience'); } // Layout suggestions const layoutTypes = new Set(results.patterns.layoutUsage.map(usage => usage.split('.')[1])); if (layoutTypes.size === 1 && layoutTypes.has('VStack')) { suggestions.push('Consider using HStack or ZStack for more dynamic layouts'); } return suggestions; } export const analyzeCommand = new Command('analyze') .description('Analyze TachUI codebase for patterns and performance') .option('-p, --pattern <pattern>', 'File pattern to analyze', 'src/**/*.{js,jsx,ts,tsx}') .option('-o, --output <file>', 'Output report to file') .option('--performance', 'Include performance analysis', true) .option('--suggestions', 'Include improvement suggestions', true) .option('--detailed', 'Show detailed component analysis', false) .option('--concatenation', 'Focus on concatenation optimization analysis', false) .action(async (options) => { try { console.log(chalk.cyan(` ╭─────────────────────────────────────╮ │ 📊 TachUI Code Analyzer │ │ Pattern analysis & metrics │ ╰─────────────────────────────────────╯ `)); const spinner = ora('Analyzing codebase...').start(); // Find files to analyze const files = await glob(options.pattern, { cwd: process.cwd(), absolute: true, }); if (files.length === 0) { spinner.fail('No files found matching pattern'); console.log(chalk.yellow(`Pattern: ${options.pattern}`)); return; } const fileAnalyses = []; const results = { files: { total: 0, byType: {}, totalSize: 0, averageSize: 0, }, components: { total: 0, withState: 0, withModifiers: 0, withLifecycle: 0, withNavigation: 0, }, patterns: { stateUsage: [], modifierUsage: [], layoutUsage: [], lifecycleUsage: [], navigationUsage: [], }, concatenation: { totalPatterns: 0, optimizedPatterns: 0, staticPatterns: 0, dynamicPatterns: 0, bundleSavingsKB: 0, accessibilityBreakdown: { minimal: 0, aria: 0, full: 0, }, recommendations: [], }, performance: { largeComponents: [], complexComponents: [], unusedImports: [], }, suggestions: [], }; // Analyze each file for (const file of files) { try { const content = readFileSync(file, 'utf-8'); const stats = statSync(file); const ext = file.split('.').pop() || 'unknown'; const analysis = analyzeFile(file, content); fileAnalyses.push(analysis); // Update file stats results.files.total++; results.files.byType[ext] = (results.files.byType[ext] || 0) + 1; results.files.totalSize += stats.size; // Update component stats results.components.total += analysis.components.length; if (analysis.hasState) results.components.withState++; if (analysis.hasModifiers) results.components.withModifiers++; if (analysis.hasLifecycle) results.components.withLifecycle++; if (analysis.hasNavigation) results.components.withNavigation++; // Collect patterns results.patterns.stateUsage.push(...analysis.stateUsage); results.patterns.modifierUsage.push(...analysis.modifierUsage); results.patterns.layoutUsage.push(...analysis.layoutUsage); results.patterns.lifecycleUsage.push(...analysis.lifecycleUsage); results.patterns.navigationUsage.push(...analysis.navigationUsage); // Performance analysis if (analysis.lines > 200) { results.performance.largeComponents.push({ file: file.split('/').pop() || file, lines: analysis.lines, size: analysis.size, }); } if (analysis.complexity > 20) { results.performance.complexComponents.push({ file: file.split('/').pop() || file, complexity: analysis.complexity, }); } } catch (_error) { // Skip files that can't be read } } results.files.averageSize = results.files.totalSize / results.files.total; // Generate concatenation analysis const allConcatenationPatterns = fileAnalyses.flatMap(f => f.concatenationPatterns); results.concatenation = generateConcatenationAnalysis(allConcatenationPatterns); // Generate suggestions if (options.suggestions) { results.suggestions = generateSuggestions(results); } spinner.succeed('Code analysis complete!'); // Display results console.log(`\n${chalk.green('📊 Analysis Results')}`); console.log(chalk.gray('─'.repeat(40))); // File statistics console.log(`\n${chalk.yellow('📁 File Statistics:')}`); console.log(`${chalk.gray('Total files:')} ${results.files.total}`); console.log(`${chalk.gray('Total size:')} ${(results.files.totalSize / 1024).toFixed(1)} KB`); console.log(`${chalk.gray('Average size:')} ${(results.files.averageSize / 1024).toFixed(1)} KB`); console.log(`\n${chalk.gray('File types:')}`); Object.entries(results.files.byType).forEach(([type, count]) => { console.log(` ${type}: ${count}`); }); // Component statistics console.log(`\n${chalk.yellow('🧩 Component Statistics:')}`); console.log(`${chalk.gray('Total components:')} ${results.components.total}`); console.log(`${chalk.gray('With @State:')} ${results.components.withState} (${((results.components.withState / results.files.total) * 100).toFixed(1)}%)`); console.log(`${chalk.gray('With modifiers:')} ${results.components.withModifiers} (${((results.components.withModifiers / results.files.total) * 100).toFixed(1)}%)`); console.log(`${chalk.gray('With lifecycle:')} ${results.components.withLifecycle} (${((results.components.withLifecycle / results.files.total) * 100).toFixed(1)}%)`); console.log(`${chalk.gray('With navigation:')} ${results.components.withNavigation} (${((results.components.withNavigation / results.files.total) * 100).toFixed(1)}%)`); // Pattern usage console.log(`\n${chalk.yellow('🎨 Pattern Usage:')}`); const topModifiers = [ ...new Set(results.patterns.modifierUsage.flat()), ].slice(0, 5); if (topModifiers.length > 0) { console.log(`${chalk.gray('Top modifiers:')} ${topModifiers.join(', ')}`); } const layoutTypes = [...new Set(results.patterns.layoutUsage)]; if (layoutTypes.length > 0) { console.log(`${chalk.gray('Layout types:')} ${layoutTypes.join(', ')}`); } const lifecycleTypes = [...new Set(results.patterns.lifecycleUsage)]; if (lifecycleTypes.length > 0) { console.log(`${chalk.gray('Lifecycle modifiers:')} ${lifecycleTypes.join(', ')}`); } // Concatenation Analysis (Enhanced when --concatenation flag is used) if (results.concatenation.totalPatterns > 0) { console.log(`\n${chalk.yellow('🔗 Concatenation Analysis:')}`); console.log(`${chalk.gray('Total patterns:')} ${results.concatenation.totalPatterns}`); console.log(`${chalk.gray('✅ Static/Optimizable:')} ${results.concatenation.optimizedPatterns} (${Math.round((results.concatenation.optimizedPatterns / results.concatenation.totalPatterns) * 100)}%)`); console.log(`${chalk.gray('🔄 Dynamic/Runtime:')} ${results.concatenation.dynamicPatterns}`); console.log(`${chalk.gray('💾 Bundle savings:')} ${results.concatenation.bundleSavingsKB.toFixed(1)}KB`); const { accessibilityBreakdown } = results.concatenation; console.log(`${chalk.gray('♿ Accessibility breakdown:')} Minimal: ${accessibilityBreakdown.minimal}, ARIA: ${accessibilityBreakdown.aria}, Full: ${accessibilityBreakdown.full}`); if (results.concatenation.recommendations.length > 0) { console.log(`${chalk.gray('💡 Recommendations:')}`); const maxRecs = options.concatenation ? results.concatenation.recommendations.length : 3; results.concatenation.recommendations .slice(0, maxRecs) .forEach((rec, index) => { console.log(` ${index + 1}. ${rec}`); }); } // Enhanced concatenation analysis when --concatenation flag is used if (options.concatenation && allConcatenationPatterns.length > 0) { console.log(`\n${chalk.cyan('📋 Detailed Concatenation Report:')}`); // Group patterns by file for detailed analysis const patternsByFile = new Map(); fileAnalyses.forEach(analysis => { if (analysis.concatenationPatterns.length > 0) { const shortFilename = analysis.file.split('/').pop() || analysis.file; patternsByFile.set(shortFilename, analysis.concatenationPatterns); } }); patternsByFile.forEach((patterns, filename) => { console.log(`\n${chalk.green(filename)}:`); patterns.forEach((pattern, index) => { const optimizationIcon = pattern.optimizable ? '✅' : '🔄'; const accessibilityColor = pattern.accessibilityNeeds === 'minimal' ? chalk.green : pattern.accessibilityNeeds === 'aria' ? chalk.yellow : chalk.red; console.log(` ${optimizationIcon} Pattern ${index + 1}: ${pattern.type} (${accessibilityColor(pattern.accessibilityNeeds)})`); console.log(` Left: ${pattern.leftComponent.substring(0, 50)}${pattern.leftComponent.length > 50 ? '...' : ''}`); console.log(` Right: ${pattern.rightComponent.substring(0, 50)}${pattern.rightComponent.length > 50 ? '...' : ''}`); }); }); // Optimization opportunities const optimizablePatterns = allConcatenationPatterns.filter(p => !p.optimizable); if (optimizablePatterns.length > 0) { console.log(`\n${chalk.yellow('🚀 Optimization Opportunities:')}`); optimizablePatterns.slice(0, 5).forEach((pattern, index) => { console.log(` ${index + 1}. Convert dynamic pattern to static in ${pattern.leftComponent}`); }); } console.log(`\n${chalk.cyan('💾 Bundle Impact Projection:')}`); console.log(` Current concatenation system: 87.76KB`); console.log(` Optimized system: ${(87.76 - results.concatenation.bundleSavingsKB).toFixed(1)}KB`); console.log(` ${chalk.green(`Savings: ${results.concatenation.bundleSavingsKB.toFixed(1)}KB (${Math.round((results.concatenation.bundleSavingsKB / 87.76) * 100)}%)`)}`); } } else if (options.concatenation) { console.log(`\n${chalk.yellow('🔗 Concatenation Analysis:')}`); console.log(chalk.gray('No concatenation patterns found in analyzed files')); console.log(chalk.gray('💡 Concatenation optimization is available when you use .build().concat() patterns')); } // Performance insights if (options.performance) { console.log(`\n${chalk.yellow('⚡ Performance Insights:')}`); if (results.performance.largeComponents.length > 0) { console.log(`${chalk.red('Large components:')} ${results.performance.largeComponents.length}`); results.performance.largeComponents.slice(0, 3).forEach(comp => { console.log(` ${comp.file}: ${comp.lines} lines`); }); } if (results.performance.complexComponents.length > 0) { console.log(`${chalk.red('Complex components:')} ${results.performance.complexComponents.length}`); results.performance.complexComponents.slice(0, 3).forEach(comp => { console.log(` ${comp.file}: complexity ${comp.complexity}`); }); } if (results.performance.largeComponents.length === 0 && results.performance.complexComponents.length === 0) { console.log(chalk.green('✅ No performance issues detected')); } } // Suggestions if (options.suggestions && results.suggestions.length > 0) { console.log(`\n${chalk.yellow('💡 Improvement Suggestions:')}`); results.suggestions.forEach((suggestion, index) => { console.log(`${chalk.gray(`${index + 1}.`)} ${suggestion}`); }); } // Detailed analysis if (options.detailed && fileAnalyses.length > 0) { console.log(`\n${chalk.yellow('🔍 Detailed Component Analysis:')}`); fileAnalyses .filter(analysis => analysis.components.length > 0) .slice(0, 5) .forEach(analysis => { const fileName = analysis.file.split('/').pop(); console.log(`\n${chalk.cyan(fileName)}:`); console.log(` Components: ${analysis.components.join(', ')}`); console.log(` Lines: ${analysis.lines}`); console.log(` Complexity: ${analysis.complexity}`); if (analysis.stateUsage.length > 0) { console.log(` State usage: ${analysis.stateUsage.length} instances`); } if (analysis.modifierUsage.length > 0) { console.log(` Modifier chains: ${analysis.modifierUsage.length}`); } }); } // TachUI health score const stateScore = (results.components.withState / results.files.total) * 25; const modifierScore = (results.components.withModifiers / results.files.total) * 25; const lifecycleScore = (results.components.withLifecycle / results.files.total) * 25; const performanceScore = Math.max(0, 25 - (results.performance.largeComponents.length + results.performance.complexComponents.length) * 5); const healthScore = Math.round(stateScore + modifierScore + lifecycleScore + performanceScore); console.log(`\n${chalk.yellow('🏥 TachUI Health Score:')} ${healthScore}/100`); let healthColor = chalk.red; if (healthScore >= 80) healthColor = chalk.green; else if (healthScore >= 60) healthColor = chalk.yellow; console.log(healthColor(`${'█'.repeat(Math.floor(healthScore / 5))}${'░'.repeat(20 - Math.floor(healthScore / 5))}`)); // Save report if requested if (options.output) { const report = JSON.stringify(results, null, 2); writeFileSync(resolve(options.output), report); console.log(`\n${chalk.green('📄 Report saved to:')} ${options.output}`); } console.log(`\n${chalk.green('Analysis complete! 🎉')}`); } catch (error) { console.error(chalk.red('Analysis error:'), error.message); process.exit(1); } }); //# sourceMappingURL=analyze.js.map