Files
gh-alekspetrov-navigator/skills/visual-regression/functions/vr_setup_validator.py
2025-11-29 17:51:59 +08:00

410 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Visual Regression Setup Validator
Detects existing Storybook setup, VR tools, CI platform, and validates component paths.
Returns comprehensive validation report to guide skill execution.
Usage:
python vr_setup_validator.py <project_root> [component_path]
"""
import json
import os
import sys
from pathlib import Path
from typing import Dict, List, Optional
def detect_framework(project_root: str) -> Optional[str]:
"""
Detect frontend framework from package.json dependencies.
Args:
project_root: Path to project root directory
Returns:
Framework name ('react', 'vue', 'svelte') or None
"""
package_json_path = Path(project_root) / 'package.json'
if not package_json_path.exists():
return None
try:
with open(package_json_path, 'r') as f:
package_data = json.load(f)
dependencies = {
**package_data.get('dependencies', {}),
**package_data.get('devDependencies', {})
}
if 'react' in dependencies:
return 'react'
elif 'vue' in dependencies:
return 'vue'
elif 'svelte' in dependencies:
return 'svelte'
return None
except (json.JSONDecodeError, FileNotFoundError):
return None
def detect_storybook_config(project_root: str) -> Dict:
"""
Detect Storybook version and configuration.
Args:
project_root: Path to project root directory
Returns:
Dict with version, addons, framework, and config path
"""
storybook_dir = Path(project_root) / '.storybook'
package_json_path = Path(project_root) / 'package.json'
result = {
'installed': False,
'version': None,
'config_path': None,
'main_js_path': None,
'addons': [],
'framework': None
}
# Check if .storybook directory exists
if not storybook_dir.exists():
return result
result['installed'] = True
result['config_path'] = str(storybook_dir)
# Check for main.js or main.ts
main_js = storybook_dir / 'main.js'
main_ts = storybook_dir / 'main.ts'
if main_js.exists():
result['main_js_path'] = str(main_js)
elif main_ts.exists():
result['main_js_path'] = str(main_ts)
# Extract version from package.json
if package_json_path.exists():
try:
with open(package_json_path, 'r') as f:
package_data = json.load(f)
dependencies = {
**package_data.get('dependencies', {}),
**package_data.get('devDependencies', {})
}
# Find Storybook version
for dep in dependencies:
if dep.startswith('@storybook/'):
result['version'] = dependencies[dep].replace('^', '').replace('~', '')
break
# Extract addons from dependencies
result['addons'] = [
dep for dep in dependencies.keys()
if dep.startswith('@storybook/addon-') or dep == '@chromatic-com/storybook'
]
except (json.JSONDecodeError, FileNotFoundError):
pass
# Try to parse main.js for framework
if result['main_js_path']:
try:
with open(result['main_js_path'], 'r') as f:
content = f.read()
if '@storybook/react' in content:
result['framework'] = 'react'
elif '@storybook/vue' in content:
result['framework'] = 'vue'
elif '@storybook/svelte' in content:
result['framework'] = 'svelte'
except FileNotFoundError:
pass
return result
def detect_vr_tool(project_root: str) -> Optional[str]:
"""
Detect existing visual regression tool from package.json.
Args:
project_root: Path to project root directory
Returns:
Tool name ('chromatic', 'percy', 'backstopjs') or None
"""
package_json_path = Path(project_root) / 'package.json'
if not package_json_path.exists():
return None
try:
with open(package_json_path, 'r') as f:
package_data = json.load(f)
dependencies = {
**package_data.get('dependencies', {}),
**package_data.get('devDependencies', {})
}
if 'chromatic' in dependencies or '@chromatic-com/storybook' in dependencies:
return 'chromatic'
elif '@percy/cli' in dependencies or '@percy/storybook' in dependencies:
return 'percy'
elif 'backstopjs' in dependencies:
return 'backstopjs'
return None
except (json.JSONDecodeError, FileNotFoundError):
return None
def detect_ci_platform(project_root: str) -> Optional[str]:
"""
Detect CI/CD platform from existing configuration files.
Args:
project_root: Path to project root directory
Returns:
Platform name ('github', 'gitlab', 'circleci', 'bitbucket') or None
"""
root = Path(project_root)
# GitHub Actions
if (root / '.github' / 'workflows').exists():
return 'github'
# GitLab CI
if (root / '.gitlab-ci.yml').exists():
return 'gitlab'
# CircleCI
if (root / '.circleci' / 'config.yml').exists():
return 'circleci'
# Bitbucket Pipelines
if (root / 'bitbucket-pipelines.yml').exists():
return 'bitbucket'
return None
def validate_component_path(component_path: str, project_root: str = '.') -> Dict:
"""
Validate component file exists and extract basic information.
Args:
component_path: Path to component file (relative or absolute)
project_root: Project root directory
Returns:
Dict with validation status and component info
"""
# Handle relative paths
if not os.path.isabs(component_path):
component_path = os.path.join(project_root, component_path)
component_file = Path(component_path)
result = {
'valid': False,
'path': component_path,
'name': None,
'extension': None,
'directory': None,
'error': None
}
# Check if file exists
if not component_file.exists():
result['error'] = f"Component file not found: {component_path}"
return result
# Check if it's a file (not directory)
if not component_file.is_file():
result['error'] = f"Path is not a file: {component_path}"
return result
# Validate extension
valid_extensions = ['.tsx', '.ts', '.jsx', '.js', '.vue', '.svelte']
if component_file.suffix not in valid_extensions:
result['error'] = f"Invalid file extension. Expected one of: {', '.join(valid_extensions)}"
return result
# Extract component name (filename without extension)
result['name'] = component_file.stem
result['extension'] = component_file.suffix
result['directory'] = str(component_file.parent)
result['valid'] = True
return result
def check_dependencies(project_root: str, vr_tool: Optional[str] = 'chromatic') -> Dict:
"""
Check which required dependencies are installed.
Args:
project_root: Path to project root directory
vr_tool: VR tool to check for ('chromatic', 'percy', 'backstopjs')
Returns:
Dict with installed and missing dependencies
"""
package_json_path = Path(project_root) / 'package.json'
result = {
'installed': [],
'missing': []
}
if not package_json_path.exists():
result['missing'] = ['package.json not found']
return result
try:
with open(package_json_path, 'r') as f:
package_data = json.load(f)
dependencies = {
**package_data.get('dependencies', {}),
**package_data.get('devDependencies', {})
}
# Core Storybook dependencies
required_deps = [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
]
# Add VR tool specific dependencies
if vr_tool == 'chromatic':
required_deps.extend(['chromatic', '@chromatic-com/storybook'])
elif vr_tool == 'percy':
required_deps.extend(['@percy/cli', '@percy/storybook'])
elif vr_tool == 'backstopjs':
required_deps.append('backstopjs')
# Check each dependency
for dep in required_deps:
if dep in dependencies:
result['installed'].append(dep)
else:
result['missing'].append(dep)
except (json.JSONDecodeError, FileNotFoundError):
result['missing'] = ['Error reading package.json']
return result
def get_package_manager(project_root: str) -> str:
"""
Detect package manager from lock files.
Args:
project_root: Path to project root directory
Returns:
Package manager name ('npm', 'yarn', 'pnpm')
"""
root = Path(project_root)
if (root / 'pnpm-lock.yaml').exists():
return 'pnpm'
elif (root / 'yarn.lock').exists():
return 'yarn'
else:
return 'npm' # Default to npm
def validate_setup(project_root: str, component_path: Optional[str] = None) -> Dict:
"""
Comprehensive validation of VR setup requirements.
Args:
project_root: Path to project root directory
component_path: Optional path to component file
Returns:
Complete validation report
"""
report = {
'project_root': project_root,
'framework': detect_framework(project_root),
'storybook': detect_storybook_config(project_root),
'vr_tool': detect_vr_tool(project_root),
'ci_platform': detect_ci_platform(project_root),
'package_manager': get_package_manager(project_root),
'component': None,
'dependencies': None,
'ready': False,
'warnings': [],
'errors': []
}
# Validate component if path provided
if component_path:
report['component'] = validate_component_path(component_path, project_root)
if not report['component']['valid']:
report['errors'].append(report['component']['error'])
# Check framework
if not report['framework']:
report['errors'].append('Framework not detected. Ensure React, Vue, or Svelte is installed.')
# Check Storybook
if not report['storybook']['installed']:
report['errors'].append('Storybook not installed. Run: npx storybook init')
# Determine VR tool (use detected or default to Chromatic)
vr_tool = report['vr_tool'] or 'chromatic'
report['dependencies'] = check_dependencies(project_root, vr_tool)
# Add warnings for missing dependencies
if report['dependencies']['missing']:
report['warnings'].append(
f"Missing dependencies: {', '.join(report['dependencies']['missing'])}"
)
# Determine if ready to proceed
report['ready'] = (
report['framework'] is not None and
report['storybook']['installed'] and
len(report['errors']) == 0
)
return report
def main():
"""CLI entry point."""
if len(sys.argv) < 2:
print("Usage: python vr_setup_validator.py <project_root> [component_path]", file=sys.stderr)
sys.exit(1)
project_root = sys.argv[1]
component_path = sys.argv[2] if len(sys.argv) > 2 else None
report = validate_setup(project_root, component_path)
# Output as JSON
print(json.dumps(report, indent=2))
# Exit with error code if not ready
sys.exit(0 if report['ready'] else 1)
if __name__ == '__main__':
main()