410 lines
12 KiB
Python
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()
|