Files
gh-epieczko-betty/skills/build.optimize/build_optimize.py
2025-11-29 18:26:08 +08:00

552 lines
18 KiB
Python
Executable File

#!/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()