Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:16:40 +08:00
commit f125e90b9f
370 changed files with 67769 additions and 0 deletions

View File

@@ -0,0 +1,369 @@
"""
Project Structure Analyzer
Analyzes React project structure against Bulletproof React patterns:
- Feature-based organization (src/features/)
- Unidirectional dependencies (shared → features → app)
- No cross-feature imports
- Proper folder hierarchy
"""
import re
from pathlib import Path
from typing import Dict, List, Set
def analyze(codebase_path: Path, metadata: Dict) -> List[Dict]:
"""
Analyze project structure for Bulletproof React compliance.
Args:
codebase_path: Path to React codebase
metadata: Project metadata from discovery phase
Returns:
List of findings with severity and migration guidance
"""
findings = []
src_dir = codebase_path / 'src'
if not src_dir.exists():
findings.append({
'severity': 'critical',
'category': 'structure',
'title': 'Missing src/ directory',
'current_state': 'No src/ directory found',
'target_state': 'All source code should be in src/ directory',
'migration_steps': [
'Create src/ directory',
'Move all source files to src/',
'Update import paths',
'Update build configuration'
],
'effort': 'medium',
})
return findings
# Check for Bulletproof structure
findings.extend(check_bulletproof_structure(src_dir))
# Check for cross-feature imports
findings.extend(check_cross_feature_imports(src_dir))
# Analyze features/ organization
findings.extend(analyze_features_directory(src_dir))
# Check shared code organization
findings.extend(check_shared_code_organization(src_dir))
# Check for architectural violations
findings.extend(check_architectural_violations(src_dir))
return findings
def check_bulletproof_structure(src_dir: Path) -> List[Dict]:
"""Check for presence of Bulletproof React folder structure."""
findings = []
# Required top-level directories for Bulletproof React
bulletproof_dirs = {
'app': 'Application layer (routes, app.tsx, provider.tsx, router.tsx)',
'features': 'Feature modules (80%+ of code should be here)',
}
# Recommended directories
recommended_dirs = {
'components': 'Shared components used across multiple features',
'hooks': 'Shared custom hooks',
'lib': 'Third-party library configurations',
'utils': 'Shared utility functions',
'types': 'Shared TypeScript types',
}
# Check required directories
for dir_name, description in bulletproof_dirs.items():
dir_path = src_dir / dir_name
if not dir_path.exists():
findings.append({
'severity': 'critical' if dir_name == 'features' else 'high',
'category': 'structure',
'title': f'Missing {dir_name}/ directory',
'current_state': f'No {dir_name}/ directory found',
'target_state': f'{dir_name}/ directory should exist: {description}',
'migration_steps': [
f'Create src/{dir_name}/ directory',
f'Organize code according to Bulletproof React {dir_name} pattern',
'Update imports to use new structure'
],
'effort': 'high' if dir_name == 'features' else 'medium',
})
# Check recommended directories (lower severity)
missing_recommended = []
for dir_name, description in recommended_dirs.items():
dir_path = src_dir / dir_name
if not dir_path.exists():
missing_recommended.append(f'{dir_name}/ ({description})')
if missing_recommended:
findings.append({
'severity': 'medium',
'category': 'structure',
'title': 'Missing recommended directories',
'current_state': f'Missing: {", ".join([d.split("/")[0] for d in missing_recommended])}',
'target_state': 'Bulletproof React recommends these directories for shared code',
'migration_steps': [
'Create missing directories as needed',
'Move shared code to appropriate directories',
'Ensure proper separation between shared and feature-specific code'
],
'effort': 'low',
})
return findings
def check_cross_feature_imports(src_dir: Path) -> List[Dict]:
"""Detect cross-feature imports (architectural violation)."""
findings = []
features_dir = src_dir / 'features'
if not features_dir.exists():
return findings
# Get all feature directories
feature_dirs = [d for d in features_dir.iterdir() if d.is_dir() and not d.name.startswith('.')]
violations = []
for feature_dir in feature_dirs:
# Find all TypeScript/JavaScript files in this feature
for file_path in feature_dir.rglob('*.{ts,tsx,js,jsx}'):
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Check for imports from other features
import_pattern = re.compile(r'from\s+[\'"]([^\'\"]+)[\'"]')
imports = import_pattern.findall(content)
for imp in imports:
# Check if importing from another feature
if imp.startswith('../') or imp.startswith('@/features/'):
# Extract feature name from import path
if '@/features/' in imp:
imported_feature = imp.split('@/features/')[1].split('/')[0]
elif '../' in imp:
# Handle relative imports
parts = imp.split('/')
if 'features' in parts:
idx = parts.index('features')
if idx + 1 < len(parts):
imported_feature = parts[idx + 1]
else:
continue
else:
continue
else:
continue
# Check if importing from different feature
current_feature = feature_dir.name
if imported_feature != current_feature and imported_feature in [f.name for f in feature_dirs]:
violations.append({
'file': str(file_path.relative_to(src_dir)),
'from_feature': current_feature,
'to_feature': imported_feature,
'import': imp
})
except:
pass
if violations:
# Group violations by feature
grouped = {}
for v in violations:
key = f"{v['from_feature']}{v['to_feature']}"
if key not in grouped:
grouped[key] = []
grouped[key].append(v['file'])
for import_path, files in grouped.items():
findings.append({
'severity': 'high',
'category': 'structure',
'title': f'Cross-feature import: {import_path}',
'current_state': f'{len(files)} file(s) import from another feature',
'target_state': 'Features should be independent. Shared code belongs in src/components/, src/hooks/, or src/utils/',
'migration_steps': [
'Identify what code is being shared between features',
'Move truly shared code to src/components/, src/hooks/, or src/utils/',
'If code is feature-specific, duplicate it or refactor feature boundaries',
'Update imports to use shared code location'
],
'effort': 'medium',
'affected_files': files[:5], # Show first 5 files
})
return findings
def analyze_features_directory(src_dir: Path) -> List[Dict]:
"""Analyze features/ directory structure."""
findings = []
features_dir = src_dir / 'features'
if not features_dir.exists():
return findings
feature_dirs = [d for d in features_dir.iterdir() if d.is_dir() and not d.name.startswith('.')]
if len(feature_dirs) == 0:
findings.append({
'severity': 'high',
'category': 'structure',
'title': 'Empty features/ directory',
'current_state': 'features/ directory exists but contains no features',
'target_state': '80%+ of application code should be organized in feature modules',
'migration_steps': [
'Identify distinct features in your application',
'Create a directory for each feature in src/features/',
'Move feature-specific code to appropriate feature directories',
'Organize each feature with api/, components/, hooks/, stores/, types/, utils/ as needed'
],
'effort': 'high',
})
return findings
# Check each feature for proper internal structure
for feature_dir in feature_dirs:
feature_name = feature_dir.name
# Recommended feature subdirectories
feature_subdirs = ['api', 'components', 'hooks', 'stores', 'types', 'utils']
has_subdirs = any((feature_dir / subdir).exists() for subdir in feature_subdirs)
# Count files in feature root
root_files = [f for f in feature_dir.iterdir() if f.is_file() and f.suffix in {'.ts', '.tsx', '.js', '.jsx'}]
if len(root_files) > 5 and not has_subdirs:
findings.append({
'severity': 'medium',
'category': 'structure',
'title': f'Feature "{feature_name}" lacks internal organization',
'current_state': f'{len(root_files)} files in feature root without subdirectories',
'target_state': 'Features should be organized with api/, components/, hooks/, stores/, types/, utils/ subdirectories',
'migration_steps': [
f'Create subdirectories in src/features/{feature_name}/',
'Move API calls to api/',
'Move components to components/',
'Move hooks to hooks/',
'Move types to types/',
'Move utilities to utils/'
],
'effort': 'low',
})
return findings
def check_shared_code_organization(src_dir: Path) -> List[Dict]:
"""Check if shared code is properly organized."""
findings = []
components_dir = src_dir / 'components'
features_dir = src_dir / 'features'
if not components_dir.exists():
return findings
# Count components
shared_components = list(components_dir.rglob('*.{tsx,jsx}'))
shared_count = len(shared_components)
# Count feature components
feature_count = 0
if features_dir.exists():
feature_count = len(list(features_dir.rglob('**/components/**/*.{tsx,jsx}')))
total_components = shared_count + feature_count
if total_components > 0:
shared_percentage = (shared_count / total_components) * 100
# Bulletproof React recommends 80%+ code in features
if shared_percentage > 40:
findings.append({
'severity': 'medium',
'category': 'structure',
'title': 'Too many shared components',
'current_state': f'{shared_percentage:.1f}% of components are in src/components/ (shared)',
'target_state': 'Most components should be feature-specific. Only truly shared components belong in src/components/',
'migration_steps': [
'Review each component in src/components/',
'Identify components used by only one feature',
'Move feature-specific components to their feature directories',
'Keep only truly shared components in src/components/'
],
'effort': 'medium',
})
return findings
def check_architectural_violations(src_dir: Path) -> List[Dict]:
"""Check for common architectural violations."""
findings = []
# Check for business logic in components/
components_dir = src_dir / 'components'
if components_dir.exists():
large_components = []
for component_file in components_dir.rglob('*.{tsx,jsx}'):
try:
with open(component_file, 'r', encoding='utf-8', errors='ignore') as f:
lines = len(f.readlines())
if lines > 200:
large_components.append((str(component_file.relative_to(src_dir)), lines))
except:
pass
if large_components:
findings.append({
'severity': 'medium',
'category': 'structure',
'title': 'Large components in shared components/',
'current_state': f'{len(large_components)} component(s) > 200 lines in src/components/',
'target_state': 'Shared components should be simple and reusable. Complex components likely belong in features/',
'migration_steps': [
'Review large shared components',
'Extract business logic to feature-specific hooks or utilities',
'Consider moving complex components to features/ if feature-specific',
'Keep shared components simple and focused'
],
'effort': 'medium',
'affected_files': [f[0] for f in large_components[:5]],
})
# Check for proper app/ structure
app_dir = src_dir / 'app'
if app_dir.exists():
expected_app_files = ['app.tsx', 'provider.tsx', 'router.tsx']
has_routing = any((app_dir / f).exists() or (app_dir / 'routes').exists() for f in ['router.tsx', 'routes.tsx'])
if not has_routing:
findings.append({
'severity': 'low',
'category': 'structure',
'title': 'Missing routing configuration in app/',
'current_state': 'No router.tsx or routes/ found in src/app/',
'target_state': 'Bulletproof React recommends centralizing routing in src/app/router.tsx or src/app/routes/',
'migration_steps': [
'Create src/app/router.tsx or src/app/routes/',
'Define all application routes in one place',
'Use code splitting for route-level lazy loading'
],
'effort': 'low',
})
return findings