#!/usr/bin/env python3 """ Build Optimization Skill Analyzes and optimizes build processes and speed across various build systems. Supports: - Webpack, Vite, Rollup, esbuild - TypeScript compilation - Node.js build processes - General build optimization strategies """ import sys import json import argparse import subprocess from pathlib import Path from typing import Dict, Any, List, Optional, Tuple import re import time # Add betty module to path from betty.logging_utils import setup_logger from betty.errors import format_error_response, BettyError from betty.telemetry_capture import telemetry_decorator logger = setup_logger(__name__) class BuildOptimizer: """Comprehensive build optimization analyzer and executor""" def __init__(self, project_path: str): """ Initialize build optimizer Args: project_path: Path to project root directory """ self.project_path = Path(project_path).resolve() if not self.project_path.exists(): raise BettyError(f"Project path does not exist: {project_path}") if not self.project_path.is_dir(): raise BettyError(f"Project path is not a directory: {project_path}") self.build_system = None self.package_json = None self.analysis_results = {} self.recommendations = [] def analyze(self, args: str = "") -> Dict[str, Any]: """ Comprehensive build analysis Args: args: Optional arguments for analysis Returns: Dict with analysis results and recommendations """ logger.info(f"Starting build optimization analysis for {self.project_path}") results = { "project_path": str(self.project_path), "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "build_system": None, "analysis": {}, "recommendations": [], "estimated_improvement": "unknown" } try: # Step 1: Identify build system build_system_info = self._identify_build_system() results["build_system"] = build_system_info logger.info(f"Build system: {build_system_info['type']}") # Step 2: Analyze dependencies dep_analysis = self._analyze_dependencies() results["analysis"]["dependencies"] = dep_analysis # Step 3: Analyze caching cache_analysis = self._analyze_caching() results["analysis"]["caching"] = cache_analysis # Step 4: Analyze bundle configuration bundle_analysis = self._analyze_bundling() results["analysis"]["bundling"] = bundle_analysis # Step 5: Analyze TypeScript configuration ts_analysis = self._analyze_typescript() results["analysis"]["typescript"] = ts_analysis # Step 6: Analyze parallelization parallel_analysis = self._analyze_parallelization() results["analysis"]["parallelization"] = parallel_analysis # Generate recommendations based on analysis recommendations = self._generate_recommendations(results["analysis"]) results["recommendations"] = recommendations # Estimate potential improvement results["estimated_improvement"] = self._estimate_improvement( results["analysis"], recommendations ) logger.info(f"Analysis complete. Found {len(recommendations)} optimization opportunities") return results except Exception as e: logger.error(f"Analysis failed: {e}") raise BettyError(f"Build analysis failed: {e}") def _identify_build_system(self) -> Dict[str, Any]: """ Step 1: Identify the build system in use """ logger.info("Identifying build system...") package_json_path = self.project_path / "package.json" if not package_json_path.exists(): return { "type": "unknown", "detected": False, "message": "No package.json found" } # Load package.json with open(package_json_path, 'r') as f: self.package_json = json.load(f) build_system = {"type": "unknown", "detected": True, "configs": []} # Check for build tools in dependencies and config files deps = { **self.package_json.get("dependencies", {}), **self.package_json.get("devDependencies", {}) } # Check for Vite if "vite" in deps or (self.project_path / "vite.config.js").exists() or \ (self.project_path / "vite.config.ts").exists(): build_system["type"] = "vite" build_system["configs"].append("vite.config.js/ts") # Check for Webpack elif "webpack" in deps or (self.project_path / "webpack.config.js").exists(): build_system["type"] = "webpack" build_system["configs"].append("webpack.config.js") # Check for Rollup elif "rollup" in deps or (self.project_path / "rollup.config.js").exists(): build_system["type"] = "rollup" build_system["configs"].append("rollup.config.js") # Check for esbuild elif "esbuild" in deps: build_system["type"] = "esbuild" # Check for TypeScript elif "typescript" in deps or (self.project_path / "tsconfig.json").exists(): build_system["type"] = "typescript" build_system["configs"].append("tsconfig.json") else: build_system["type"] = "generic" # Check build scripts scripts = self.package_json.get("scripts", {}) build_system["scripts"] = { "build": scripts.get("build"), "dev": scripts.get("dev"), "test": scripts.get("test") } return build_system def _analyze_dependencies(self) -> Dict[str, Any]: """ Step 2: Analyze build dependencies and their impact """ logger.info("Analyzing dependencies...") if not self.package_json: return {"analyzed": False, "message": "No package.json"} deps = self.package_json.get("dependencies", {}) dev_deps = self.package_json.get("devDependencies", {}) analysis = { "total_dependencies": len(deps), "total_dev_dependencies": len(dev_deps), "outdated": [], "unused": [], "large_dependencies": [], "recommendations": [] } # Check for common heavy dependencies heavy_deps = ["moment", "lodash", "core-js"] for dep in heavy_deps: if dep in deps: analysis["large_dependencies"].append({ "name": dep, "suggestion": f"Consider replacing {dep} with lighter alternative" }) # Recommendations if "moment" in deps: analysis["recommendations"].append( "Replace 'moment' with 'date-fns' or 'dayjs' for smaller bundle size" ) if "lodash" in deps: analysis["recommendations"].append( "Use 'lodash-es' with tree-shaking or import specific lodash functions" ) return analysis def _analyze_caching(self) -> Dict[str, Any]: """ Step 3: Analyze caching strategy """ logger.info("Analyzing caching strategy...") analysis = { "cache_enabled": False, "cache_type": "none", "recommendations": [] } # Check for cache directories cache_dirs = [ ".cache", "node_modules/.cache", ".webpack-cache", ".vite" ] for cache_dir in cache_dirs: if (self.project_path / cache_dir).exists(): analysis["cache_enabled"] = True analysis["cache_type"] = cache_dir break if not analysis["cache_enabled"]: analysis["recommendations"].append( "Enable persistent caching for faster incremental builds" ) # Check for CI cache configuration if (self.project_path / ".github" / "workflows").exists(): analysis["ci_cache"] = "github-actions" return analysis def _analyze_bundling(self) -> Dict[str, Any]: """ Step 4: Analyze bundle configuration """ logger.info("Analyzing bundling configuration...") analysis = { "code_splitting": "unknown", "tree_shaking": "unknown", "minification": "unknown", "recommendations": [] } # Check for build output dist_dir = self.project_path / "dist" build_dir = self.project_path / "build" output_dir = dist_dir if dist_dir.exists() else build_dir if output_dir and output_dir.exists(): js_files = list(output_dir.glob("**/*.js")) analysis["output_files"] = len(js_files) # Estimate if code splitting is used if len(js_files) > 3: analysis["code_splitting"] = "enabled" elif len(js_files) <= 1: analysis["code_splitting"] = "disabled" analysis["recommendations"].append( "Enable code splitting to reduce initial bundle size" ) return analysis def _analyze_typescript(self) -> Dict[str, Any]: """ Step 5: Analyze TypeScript configuration """ logger.info("Analyzing TypeScript configuration...") tsconfig_path = self.project_path / "tsconfig.json" if not tsconfig_path.exists(): return { "enabled": False, "message": "No TypeScript configuration found" } with open(tsconfig_path, 'r') as f: # Remove comments from JSON (basic approach) content = f.read() content = re.sub(r'//.*?\n', '\n', content) content = re.sub(r'/\*.*?\*/', '', content, flags=re.DOTALL) tsconfig = json.loads(content) compiler_options = tsconfig.get("compilerOptions", {}) analysis = { "enabled": True, "incremental": compiler_options.get("incremental", False), "skipLibCheck": compiler_options.get("skipLibCheck", False), "composite": compiler_options.get("composite", False), "recommendations": [] } # Recommendations for faster compilation if not analysis["incremental"]: analysis["recommendations"].append( "Enable 'incremental: true' in tsconfig.json for faster rebuilds" ) if not analysis["skipLibCheck"]: analysis["recommendations"].append( "Enable 'skipLibCheck: true' to skip type checking of declaration files" ) return analysis def _analyze_parallelization(self) -> Dict[str, Any]: """ Step 6: Analyze parallel processing opportunities """ logger.info("Analyzing parallelization opportunities...") analysis = { "cpu_cores": self._get_cpu_count(), "parallel_build": "unknown", "recommendations": [] } if self.build_system and self.build_system.get("type") == "webpack": analysis["recommendations"].append( "Consider using 'thread-loader' for parallel processing in Webpack" ) if self.build_system and self.build_system.get("type") == "typescript": analysis["recommendations"].append( "Use 'ts-loader' with 'transpileOnly: true' or 'esbuild-loader' for faster TypeScript compilation" ) return analysis def _generate_recommendations(self, analysis: Dict[str, Any]) -> List[Dict[str, Any]]: """ Generate prioritized recommendations based on analysis """ recommendations = [] # Collect all recommendations from analysis for section, data in analysis.items(): if isinstance(data, dict) and "recommendations" in data: for rec in data["recommendations"]: recommendations.append({ "category": section, "priority": "medium", "description": rec }) # Add high-priority recommendations if analysis.get("caching", {}).get("cache_enabled") == False: recommendations.insert(0, { "category": "caching", "priority": "high", "description": "Enable persistent caching for significant build speed improvements" }) if analysis.get("typescript", {}).get("incremental") == False: recommendations.insert(0, { "category": "typescript", "priority": "high", "description": "Enable incremental TypeScript compilation" }) return recommendations def _estimate_improvement( self, analysis: Dict[str, Any], recommendations: List[Dict[str, Any]] ) -> str: """ Estimate potential build time improvement """ high_priority = sum(1 for r in recommendations if r.get("priority") == "high") total = len(recommendations) if high_priority >= 3: return "40-60% faster (multiple high-impact optimizations)" elif high_priority >= 1: return "20-40% faster (some high-impact optimizations)" elif total >= 5: return "10-20% faster (many small optimizations)" elif total >= 1: return "5-10% faster (few optimizations available)" else: return "Already well optimized" def _get_cpu_count(self) -> int: """Get number of CPU cores""" try: import os return os.cpu_count() or 1 except: return 1 def apply_optimizations(self, recommendations: List[Dict[str, Any]]) -> Dict[str, Any]: """ Apply recommended optimizations (interactive mode) Args: recommendations: List of recommendations to apply Returns: Results of optimization application """ results = { "applied": [], "skipped": [], "failed": [] } logger.info("Optimization application would happen here in full implementation") logger.info("This is a demonstration skill showing the structure") return results @telemetry_decorator(skill_name="build.optimize") def main(): """CLI entry point""" parser = argparse.ArgumentParser( description="Analyze and optimize build processes" ) parser.add_argument( "project_path", nargs="?", default=".", help="Path to project root (default: current directory)" ) parser.add_argument( "--format", choices=["json", "human"], default="human", help="Output format (default: human)" ) parser.add_argument( "--apply", action="store_true", help="Apply recommended optimizations (interactive)" ) args = parser.parse_args() try: # Create optimizer optimizer = BuildOptimizer(args.project_path) # Run analysis results = optimizer.analyze() # Output results if args.format == "json": print(json.dumps(results, indent=2)) else: # Human-readable output print(f"\nšŸ” Build Optimization Analysis") print(f"=" * 60) print(f"Project: {results['project_path']}") print(f"Build System: {results['build_system']['type']}") print() # Dependencies if "dependencies" in results["analysis"]: dep = results["analysis"]["dependencies"] print(f"šŸ“¦ Dependencies:") print(f" Total: {dep.get('total_dependencies', 0)}") print(f" Dev: {dep.get('total_dev_dependencies', 0)}") if dep.get("large_dependencies"): print(f" Large deps: {len(dep['large_dependencies'])}") print() # Caching if "caching" in results["analysis"]: cache = results["analysis"]["caching"] print(f"šŸ’¾ Caching:") print(f" Enabled: {cache.get('cache_enabled', False)}") print(f" Type: {cache.get('cache_type', 'none')}") print() # TypeScript if "typescript" in results["analysis"]: ts = results["analysis"]["typescript"] if ts.get("enabled"): print(f"šŸ“˜ TypeScript:") print(f" Incremental: {ts.get('incremental', False)}") print(f" Skip Lib Check: {ts.get('skipLibCheck', False)}") print() # Recommendations if results["recommendations"]: print(f"šŸ’” Recommendations ({len(results['recommendations'])}):") print() for i, rec in enumerate(results["recommendations"], 1): priority_emoji = "šŸ”“" if rec['priority'] == "high" else "🟔" print(f" {i}. {priority_emoji} {rec['description']}") print(f" Category: {rec['category']}") print() print(f"⚔ Estimated Improvement: {results['estimated_improvement']}") print() if args.apply: print("Would you like to apply these optimizations?") print("(Interactive application not yet implemented)") sys.exit(0) except BettyError as e: print(format_error_response(str(e), "build.optimize")) sys.exit(1) except Exception as e: logger.error(f"Unexpected error: {e}", exc_info=True) print(format_error_response(f"Unexpected error: {e}", "build.optimize")) sys.exit(1) if __name__ == "__main__": main()