Replace placeholder content with real frontend development guidance: References: - react_patterns.md: Compound Components, Render Props, Custom Hooks - nextjs_optimization_guide.md: Server/Client Components, ISR, caching - frontend_best_practices.md: Accessibility, testing, TypeScript patterns Scripts: - frontend_scaffolder.py: Generate Next.js/React projects with features - component_generator.py: Generate React components with tests/stories - bundle_analyzer.py: Analyze package.json for optimization opportunities SKILL.md: - Added table of contents - Numbered workflow steps - Removed marketing language - Added trigger phrases in description Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
408 lines
13 KiB
Python
Executable File
408 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Frontend Bundle Analyzer
|
|
|
|
Analyzes package.json and project structure for bundle optimization opportunities,
|
|
heavy dependencies, and best practice recommendations.
|
|
|
|
Usage:
|
|
python bundle_analyzer.py <project_dir>
|
|
python bundle_analyzer.py . --json
|
|
python bundle_analyzer.py /path/to/project --verbose
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Any, Tuple
|
|
|
|
|
|
# Known heavy packages and their lighter alternatives
|
|
HEAVY_PACKAGES = {
|
|
"moment": {
|
|
"size": "290KB",
|
|
"alternative": "date-fns (12KB) or dayjs (2KB)",
|
|
"reason": "Large locale files bundled by default"
|
|
},
|
|
"lodash": {
|
|
"size": "71KB",
|
|
"alternative": "lodash-es with tree-shaking or individual imports (lodash/get)",
|
|
"reason": "Full library often imported when only few functions needed"
|
|
},
|
|
"jquery": {
|
|
"size": "87KB",
|
|
"alternative": "Native DOM APIs or React/Vue patterns",
|
|
"reason": "Rarely needed in modern frameworks"
|
|
},
|
|
"axios": {
|
|
"size": "14KB",
|
|
"alternative": "Native fetch API (0KB) or ky (3KB)",
|
|
"reason": "Fetch API covers most use cases"
|
|
},
|
|
"underscore": {
|
|
"size": "17KB",
|
|
"alternative": "Native ES6+ methods or lodash-es",
|
|
"reason": "Most utilities now in standard JavaScript"
|
|
},
|
|
"chart.js": {
|
|
"size": "180KB",
|
|
"alternative": "recharts (bundled with React) or lightweight-charts",
|
|
"reason": "Consider if you need all chart types"
|
|
},
|
|
"three": {
|
|
"size": "600KB",
|
|
"alternative": "None - use dynamic import for 3D features",
|
|
"reason": "Very large, should be lazy-loaded"
|
|
},
|
|
"firebase": {
|
|
"size": "400KB+",
|
|
"alternative": "Import specific modules (firebase/auth, firebase/firestore)",
|
|
"reason": "Modular imports significantly reduce size"
|
|
},
|
|
"material-ui": {
|
|
"size": "Large",
|
|
"alternative": "shadcn/ui (copy-paste components) or Tailwind",
|
|
"reason": "Heavy runtime, consider headless alternatives"
|
|
},
|
|
"@mui/material": {
|
|
"size": "Large",
|
|
"alternative": "shadcn/ui or Radix UI + Tailwind",
|
|
"reason": "Heavy runtime, consider headless alternatives"
|
|
},
|
|
"antd": {
|
|
"size": "Large",
|
|
"alternative": "shadcn/ui or Radix UI + Tailwind",
|
|
"reason": "Heavy runtime, consider headless alternatives"
|
|
}
|
|
}
|
|
|
|
# Recommended optimizations by package
|
|
PACKAGE_OPTIMIZATIONS = {
|
|
"react-icons": "Import individual icons: import { FaHome } from 'react-icons/fa'",
|
|
"date-fns": "Use tree-shaking: import { format } from 'date-fns'",
|
|
"@heroicons/react": "Already tree-shakeable, good choice",
|
|
"lucide-react": "Already tree-shakeable, add to optimizePackageImports in next.config.js",
|
|
"framer-motion": "Use dynamic import for non-critical animations",
|
|
"recharts": "Consider lazy loading for dashboard charts",
|
|
}
|
|
|
|
# Development dependencies that should not be in dependencies
|
|
DEV_ONLY_PACKAGES = [
|
|
"typescript", "@types/", "eslint", "prettier", "jest", "vitest",
|
|
"@testing-library", "cypress", "playwright", "storybook", "@storybook",
|
|
"webpack", "vite", "rollup", "esbuild", "tailwindcss", "postcss",
|
|
"autoprefixer", "sass", "less", "husky", "lint-staged"
|
|
]
|
|
|
|
|
|
def load_package_json(project_dir: Path) -> Optional[Dict]:
|
|
"""Load and parse package.json."""
|
|
package_path = project_dir / "package.json"
|
|
if not package_path.exists():
|
|
return None
|
|
|
|
try:
|
|
with open(package_path) as f:
|
|
return json.load(f)
|
|
except json.JSONDecodeError:
|
|
return None
|
|
|
|
|
|
def analyze_dependencies(package_json: Dict) -> Dict:
|
|
"""Analyze dependencies for issues."""
|
|
deps = package_json.get("dependencies", {})
|
|
dev_deps = package_json.get("devDependencies", {})
|
|
|
|
issues = []
|
|
warnings = []
|
|
optimizations = []
|
|
|
|
# Check for heavy packages
|
|
for pkg, info in HEAVY_PACKAGES.items():
|
|
if pkg in deps:
|
|
issues.append({
|
|
"package": pkg,
|
|
"type": "heavy_dependency",
|
|
"size": info["size"],
|
|
"alternative": info["alternative"],
|
|
"reason": info["reason"]
|
|
})
|
|
|
|
# Check for dev dependencies in production
|
|
for pkg in deps.keys():
|
|
for dev_pattern in DEV_ONLY_PACKAGES:
|
|
if dev_pattern in pkg:
|
|
warnings.append({
|
|
"package": pkg,
|
|
"type": "dev_in_production",
|
|
"message": f"{pkg} should be in devDependencies, not dependencies"
|
|
})
|
|
|
|
# Check for optimization opportunities
|
|
for pkg in deps.keys():
|
|
for opt_pkg, opt_tip in PACKAGE_OPTIMIZATIONS.items():
|
|
if opt_pkg in pkg:
|
|
optimizations.append({
|
|
"package": pkg,
|
|
"tip": opt_tip
|
|
})
|
|
|
|
# Check for outdated React patterns
|
|
if "prop-types" in deps and ("typescript" in dev_deps or "@types/react" in dev_deps):
|
|
warnings.append({
|
|
"package": "prop-types",
|
|
"type": "redundant",
|
|
"message": "prop-types is redundant when using TypeScript"
|
|
})
|
|
|
|
# Check for multiple state management libraries
|
|
state_libs = ["redux", "@reduxjs/toolkit", "mobx", "zustand", "jotai", "recoil", "valtio"]
|
|
found_state_libs = [lib for lib in state_libs if lib in deps]
|
|
if len(found_state_libs) > 1:
|
|
warnings.append({
|
|
"packages": found_state_libs,
|
|
"type": "multiple_state_libs",
|
|
"message": f"Multiple state management libraries found: {', '.join(found_state_libs)}"
|
|
})
|
|
|
|
return {
|
|
"total_dependencies": len(deps),
|
|
"total_dev_dependencies": len(dev_deps),
|
|
"issues": issues,
|
|
"warnings": warnings,
|
|
"optimizations": optimizations
|
|
}
|
|
|
|
|
|
def check_nextjs_config(project_dir: Path) -> Dict:
|
|
"""Check Next.js configuration for optimizations."""
|
|
config_paths = [
|
|
project_dir / "next.config.js",
|
|
project_dir / "next.config.mjs",
|
|
project_dir / "next.config.ts"
|
|
]
|
|
|
|
for config_path in config_paths:
|
|
if config_path.exists():
|
|
try:
|
|
content = config_path.read_text()
|
|
suggestions = []
|
|
|
|
# Check for image optimization
|
|
if "images" not in content:
|
|
suggestions.append("Configure images.remotePatterns for optimized image loading")
|
|
|
|
# Check for package optimization
|
|
if "optimizePackageImports" not in content:
|
|
suggestions.append("Add experimental.optimizePackageImports for lucide-react, @heroicons/react")
|
|
|
|
# Check for transpilePackages
|
|
if "transpilePackages" not in content and "swc" not in content:
|
|
suggestions.append("Consider transpilePackages for monorepo packages")
|
|
|
|
return {
|
|
"found": True,
|
|
"path": str(config_path),
|
|
"suggestions": suggestions
|
|
}
|
|
except Exception:
|
|
pass
|
|
|
|
return {
|
|
"found": False,
|
|
"suggestions": ["Create next.config.js with image and bundle optimizations"]
|
|
}
|
|
|
|
|
|
def analyze_imports(project_dir: Path) -> Dict:
|
|
"""Analyze import patterns in source files."""
|
|
issues = []
|
|
src_dirs = [project_dir / "src", project_dir / "app", project_dir / "pages"]
|
|
|
|
patterns_to_check = [
|
|
(r"import\s+\*\s+as\s+\w+\s+from\s+['\"]lodash['\"]", "Avoid import * from lodash, use individual imports"),
|
|
(r"import\s+moment\s+from\s+['\"]moment['\"]", "Consider replacing moment with date-fns or dayjs"),
|
|
(r"import\s+\{\s*\w+(?:,\s*\w+){5,}\s*\}\s+from\s+['\"]react-icons", "Import icons from specific icon sets (react-icons/fa)"),
|
|
]
|
|
|
|
files_checked = 0
|
|
for src_dir in src_dirs:
|
|
if not src_dir.exists():
|
|
continue
|
|
|
|
for ext in ["*.ts", "*.tsx", "*.js", "*.jsx"]:
|
|
for file_path in src_dir.glob(f"**/{ext}"):
|
|
if "node_modules" in str(file_path):
|
|
continue
|
|
|
|
files_checked += 1
|
|
try:
|
|
content = file_path.read_text()
|
|
for pattern, message in patterns_to_check:
|
|
if re.search(pattern, content):
|
|
issues.append({
|
|
"file": str(file_path.relative_to(project_dir)),
|
|
"issue": message
|
|
})
|
|
except Exception:
|
|
continue
|
|
|
|
return {
|
|
"files_checked": files_checked,
|
|
"issues": issues
|
|
}
|
|
|
|
|
|
def calculate_score(analysis: Dict) -> Tuple[int, str]:
|
|
"""Calculate bundle health score."""
|
|
score = 100
|
|
|
|
# Deduct for heavy dependencies
|
|
score -= len(analysis["dependencies"]["issues"]) * 10
|
|
|
|
# Deduct for dev deps in production
|
|
score -= len([w for w in analysis["dependencies"]["warnings"]
|
|
if w.get("type") == "dev_in_production"]) * 5
|
|
|
|
# Deduct for import issues
|
|
score -= len(analysis.get("imports", {}).get("issues", [])) * 3
|
|
|
|
# Deduct for missing Next.js optimizations
|
|
if not analysis.get("nextjs", {}).get("found", True):
|
|
score -= 10
|
|
|
|
score = max(0, min(100, score))
|
|
|
|
if score >= 90:
|
|
grade = "A"
|
|
elif score >= 80:
|
|
grade = "B"
|
|
elif score >= 70:
|
|
grade = "C"
|
|
elif score >= 60:
|
|
grade = "D"
|
|
else:
|
|
grade = "F"
|
|
|
|
return score, grade
|
|
|
|
|
|
def print_report(analysis: Dict) -> None:
|
|
"""Print human-readable report."""
|
|
score, grade = calculate_score(analysis)
|
|
|
|
print("=" * 60)
|
|
print("FRONTEND BUNDLE ANALYSIS REPORT")
|
|
print("=" * 60)
|
|
print(f"\nBundle Health Score: {score}/100 ({grade})")
|
|
|
|
deps = analysis["dependencies"]
|
|
print(f"\nDependencies: {deps['total_dependencies']} production, {deps['total_dev_dependencies']} dev")
|
|
|
|
# Heavy dependencies
|
|
if deps["issues"]:
|
|
print("\n--- HEAVY DEPENDENCIES ---")
|
|
for issue in deps["issues"]:
|
|
print(f"\n {issue['package']} ({issue['size']})")
|
|
print(f" Reason: {issue['reason']}")
|
|
print(f" Alternative: {issue['alternative']}")
|
|
|
|
# Warnings
|
|
if deps["warnings"]:
|
|
print("\n--- WARNINGS ---")
|
|
for warning in deps["warnings"]:
|
|
if "package" in warning:
|
|
print(f" - {warning['package']}: {warning['message']}")
|
|
else:
|
|
print(f" - {warning['message']}")
|
|
|
|
# Optimizations
|
|
if deps["optimizations"]:
|
|
print("\n--- OPTIMIZATION TIPS ---")
|
|
for opt in deps["optimizations"]:
|
|
print(f" - {opt['package']}: {opt['tip']}")
|
|
|
|
# Next.js config
|
|
if "nextjs" in analysis:
|
|
nextjs = analysis["nextjs"]
|
|
if nextjs.get("suggestions"):
|
|
print("\n--- NEXT.JS CONFIG ---")
|
|
for suggestion in nextjs["suggestions"]:
|
|
print(f" - {suggestion}")
|
|
|
|
# Import issues
|
|
if analysis.get("imports", {}).get("issues"):
|
|
print("\n--- IMPORT ISSUES ---")
|
|
for issue in analysis["imports"]["issues"][:10]: # Limit to 10
|
|
print(f" - {issue['file']}: {issue['issue']}")
|
|
|
|
# Summary
|
|
print("\n--- RECOMMENDATIONS ---")
|
|
if score >= 90:
|
|
print(" Bundle is well-optimized!")
|
|
elif deps["issues"]:
|
|
print(" 1. Replace heavy dependencies with lighter alternatives")
|
|
if deps["warnings"]:
|
|
print(" 2. Move dev-only packages to devDependencies")
|
|
if deps["optimizations"]:
|
|
print(" 3. Apply import optimizations for tree-shaking")
|
|
|
|
print("\n" + "=" * 60)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Analyze frontend project for bundle optimization opportunities"
|
|
)
|
|
parser.add_argument(
|
|
"project_dir",
|
|
nargs="?",
|
|
default=".",
|
|
help="Project directory to analyze (default: current directory)"
|
|
)
|
|
parser.add_argument(
|
|
"--json",
|
|
action="store_true",
|
|
help="Output in JSON format"
|
|
)
|
|
parser.add_argument(
|
|
"--verbose", "-v",
|
|
action="store_true",
|
|
help="Include detailed import analysis"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
project_dir = Path(args.project_dir).resolve()
|
|
|
|
if not project_dir.exists():
|
|
print(f"Error: Directory not found: {project_dir}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
package_json = load_package_json(project_dir)
|
|
if not package_json:
|
|
print("Error: No valid package.json found", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
analysis = {
|
|
"project": str(project_dir),
|
|
"dependencies": analyze_dependencies(package_json),
|
|
"nextjs": check_nextjs_config(project_dir)
|
|
}
|
|
|
|
if args.verbose:
|
|
analysis["imports"] = analyze_imports(project_dir)
|
|
|
|
analysis["score"], analysis["grade"] = calculate_score(analysis)
|
|
|
|
if args.json:
|
|
print(json.dumps(analysis, indent=2))
|
|
else:
|
|
print_report(analysis)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|