#!/usr/bin/env python3 """ Chromatic Configuration Generator Generates Chromatic config files, updates Storybook configuration, and adds package.json scripts for visual regression testing. Usage: python chromatic_config_generator.py [vr_tool] """ import json import os import re import sys from pathlib import Path from typing import Dict, Optional def generate_chromatic_config(project_info: Dict) -> str: """ Generate chromatic.config.json content. Args: project_info: Project information (main_branch, etc.) Returns: JSON config as string """ main_branch = project_info.get('main_branch', 'main') config = { "projectId": "", "buildScriptName": "build-storybook", "exitZeroOnChanges": True, "exitOnceUploaded": True, "onlyChanged": True, "externals": ["public/**"], "skip": "dependabot/**", "ignoreLastBuildOnBranch": main_branch } return json.dumps(config, indent=2) def generate_percy_config(project_info: Dict) -> str: """ Generate .percy.yml content for Percy. Args: project_info: Project information Returns: YAML config as string """ config = """version: 2 static: build-dir: storybook-static clean-urls: false snapshot: widths: - 375 - 768 - 1280 min-height: 1024 percy-css: '' """ return config def generate_backstop_config(project_info: Dict) -> str: """ Generate backstop.config.js for BackstopJS. Args: project_info: Project information Returns: JS config as string """ config = """module.exports = { id: 'backstop_default', viewports: [ { label: 'phone', width: 375, height: 667 }, { label: 'tablet', width: 768, height: 1024 }, { label: 'desktop', width: 1280, height: 1024 } ], scenarios: [], paths: { bitmaps_reference: 'backstop_data/bitmaps_reference', bitmaps_test: 'backstop_data/bitmaps_test', engine_scripts: 'backstop_data/engine_scripts', html_report: 'backstop_data/html_report', ci_report: 'backstop_data/ci_report' }, report: ['browser'], engine: 'puppeteer', engineOptions: { args: ['--no-sandbox'] }, asyncCaptureLimit: 5, asyncCompareLimit: 50, debug: false, debugWindow: false }; """ return config def update_storybook_main_config(main_js_path: str, vr_tool: str = 'chromatic') -> str: """ Update .storybook/main.js to include VR tool addon. Args: main_js_path: Path to main.js file vr_tool: VR tool name ('chromatic', 'percy', 'backstopjs') Returns: Updated main.js content """ # Read existing config if not os.path.exists(main_js_path): # Generate new config if doesn't exist return generate_new_storybook_config(vr_tool) with open(main_js_path, 'r') as f: content = f.read() # Determine addon to add if vr_tool == 'chromatic': addon = '@chromatic-com/storybook' elif vr_tool == 'percy': addon = '@percy/storybook' else: return content # BackstopJS doesn't need addon # Check if addon already exists if addon in content: return content # Already configured # Find addons array and insert addons_pattern = r'addons:\s*\[(.*?)\]' match = re.search(addons_pattern, content, re.DOTALL) if match: existing_addons = match.group(1).strip() # Add new addon updated_addons = f"{existing_addons},\n '{addon}'" updated_content = content.replace(match.group(0), f"addons: [\n {updated_addons}\n ]") return updated_content else: # No addons array found - append at end return content + f"\n// Added by Navigator visual-regression skill\nmodule.exports.addons.push('{addon}');\n" def generate_new_storybook_config(vr_tool: str = 'chromatic') -> str: """ Generate new .storybook/main.js from scratch. Args: vr_tool: VR tool name Returns: main.js content """ addon = '@chromatic-com/storybook' if vr_tool == 'chromatic' else '@percy/storybook' config = f"""module.exports = {{ stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ '@storybook/addon-links', '@storybook/addon-essentials', '{addon}', '@storybook/addon-interactions', ], framework: {{ name: '@storybook/react-vite', options: {{}}, }}, }}; """ return config def update_package_json_scripts(package_json_path: str, vr_tool: str = 'chromatic') -> Dict: """ Add VR tool scripts to package.json. Args: package_json_path: Path to package.json vr_tool: VR tool name Returns: Updated package.json data """ with open(package_json_path, 'r') as f: package_data = json.load(f) scripts = package_data.get('scripts', {}) # Add VR tool scripts if vr_tool == 'chromatic': scripts['chromatic'] = 'npx chromatic' scripts['chromatic:ci'] = 'npx chromatic --exit-zero-on-changes' elif vr_tool == 'percy': scripts['percy'] = 'percy storybook storybook-static' scripts['percy:ci'] = 'percy storybook storybook-static --partial' elif vr_tool == 'backstopjs': scripts['backstop:reference'] = 'backstop reference' scripts['backstop:test'] = 'backstop test' scripts['backstop:approve'] = 'backstop approve' # Ensure build-storybook script exists if 'build-storybook' not in scripts: scripts['build-storybook'] = 'storybook build' package_data['scripts'] = scripts return package_data def detect_main_branch(project_root: str) -> str: """ Detect main branch name from git. Args: project_root: Project root directory Returns: Branch name ('main' or 'master') """ git_head = Path(project_root) / '.git' / 'HEAD' if git_head.exists(): with open(git_head, 'r') as f: content = f.read().strip() if 'refs/heads/main' in content: return 'main' elif 'refs/heads/master' in content: return 'master' return 'main' # Default def generate_configs(project_root: str, vr_tool: str = 'chromatic') -> Dict: """ Generate all configuration files for VR setup. Args: project_root: Project root directory vr_tool: VR tool to configure Returns: Dict with file paths and contents """ project_info = { 'main_branch': detect_main_branch(project_root) } result = { 'configs_generated': [], 'configs_updated': [], 'errors': [] } # Generate tool-specific config if vr_tool == 'chromatic': config_path = os.path.join(project_root, 'chromatic.config.json') config_content = generate_chromatic_config(project_info) result['configs_generated'].append({ 'path': config_path, 'content': config_content }) elif vr_tool == 'percy': config_path = os.path.join(project_root, '.percy.yml') config_content = generate_percy_config(project_info) result['configs_generated'].append({ 'path': config_path, 'content': config_content }) elif vr_tool == 'backstopjs': config_path = os.path.join(project_root, 'backstop.config.js') config_content = generate_backstop_config(project_info) result['configs_generated'].append({ 'path': config_path, 'content': config_content }) # Update Storybook main.js storybook_dir = Path(project_root) / '.storybook' main_js_candidates = [ storybook_dir / 'main.js', storybook_dir / 'main.ts' ] main_js_path = None for candidate in main_js_candidates: if candidate.exists(): main_js_path = str(candidate) break if main_js_path: main_js_content = update_storybook_main_config(main_js_path, vr_tool) result['configs_updated'].append({ 'path': main_js_path, 'content': main_js_content }) elif storybook_dir.exists(): # Create new main.js main_js_path = str(storybook_dir / 'main.js') main_js_content = generate_new_storybook_config(vr_tool) result['configs_generated'].append({ 'path': main_js_path, 'content': main_js_content }) # Update package.json package_json_path = os.path.join(project_root, 'package.json') if os.path.exists(package_json_path): updated_package = update_package_json_scripts(package_json_path, vr_tool) result['configs_updated'].append({ 'path': package_json_path, 'content': json.dumps(updated_package, indent=2) }) return result def main(): """CLI entry point.""" if len(sys.argv) < 2: print("Usage: python chromatic_config_generator.py [vr_tool]", file=sys.stderr) sys.exit(1) project_root = sys.argv[1] vr_tool = sys.argv[2] if len(sys.argv) > 2 else 'chromatic' if vr_tool not in ['chromatic', 'percy', 'backstopjs']: print(f"Unsupported VR tool: {vr_tool}. Use: chromatic, percy, or backstopjs", file=sys.stderr) sys.exit(1) result = generate_configs(project_root, vr_tool) # Output as JSON print(json.dumps(result, indent=2)) if __name__ == '__main__': main()