Files
opencode-skill/skills/website-creator/scripts/migrate_existing_website.py
Kunthawat Greethong 61321669d1 fix: Add missing imports to migrate_existing_website.py
- Added argparse, json, shutil, re, subprocess imports
- Fixed NameError when running script
- Script now works correctly
2026-03-12 09:45:48 +07:00

562 lines
21 KiB
Python

#!/usr/bin/env python3
"""
Smart Website Migration - Detect, Plan, then Migrate
This script intelligently migrates existing websites by:
1. Detecting current tech stack and versions
2. Creating a detailed migration plan
3. Preserving ALL inline CSS and content exactly
4. Converting CSS frameworks (Tailwind v3 → v4, etc.)
5. Reinstalling Astro fresh
6. Adding new features without breaking existing functionality
Workflow:
1. ANALYZE - Detect tech stack, versions, CSS framework
2. PLAN - Create detailed migration plan
3. BACKUP - Create full backup
4. PRESERVE - Extract inline CSS and content from each page
5. CONVERT - Convert CSS to match target tech stack
6. REBUILD - Fresh Astro install with preserved content
7. ENHANCE - Add new features (cookie consent, PDPA, etc.)
8. TEST - Verify build and all pages
Usage:
python3 migrate_existing_website.py \
--input "./existing-website" \
--output "./migrated-website" \
--plan-only # Just create plan, don't migrate
"""
import os
import sys
import json
import shutil
import re
import subprocess
import argparse
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional
import sys
import json
import shutil
import re
import subprocess
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Any, Optional
class TechStackDetector:
"""Detect tech stack and versions from existing website."""
def __init__(self, website_path: Path):
self.website_path = website_path
self.detected = {}
def detect_all(self) -> Dict[str, Any]:
"""Run all detection methods."""
print("🔍 Detecting tech stack...\n")
self.detect_astro_version()
self.detect_node_version()
self.detect_css_framework()
self.detect_tailwind_version()
self.detect_pages_structure()
self.detect_content_collections()
self.detect_integrations()
self.detect_custom_css()
return self.detected
def detect_astro_version(self):
"""Detect Astro version from package.json."""
package_json = self.website_path / 'package.json'
if package_json.exists():
with open(package_json) as f:
package_data = json.load(f)
deps = package_data.get('dependencies', {})
dev_deps = package_data.get('devDependencies', {})
astro_version = deps.get('astro') or dev_deps.get('astro')
self.detected['astro'] = {
'version': astro_version or 'unknown',
'detected': True
}
print(f" ✓ Astro version: {astro_version}")
else:
print(f" ✗ package.json not found")
self.detected['astro'] = {'version': 'unknown', 'detected': False}
def detect_node_version(self):
"""Detect required Node.js version."""
package_json = self.website_path / 'package.json'
if package_json.exists():
with open(package_json) as f:
package_data = json.load(f)
engines = package_data.get('engines', {})
node_version = engines.get('node', '>=18.0.0')
self.detected['node'] = {
'required_version': node_version,
'detected': True
}
print(f" ✓ Node.js: {node_version}")
def detect_css_framework(self):
"""Detect CSS framework (Tailwind, Bootstrap, etc.)."""
package_json = self.website_path / 'package.json'
css_frameworks = {
'tailwindcss': 'Tailwind CSS',
'bootstrap': 'Bootstrap',
'bulma': 'Bulma',
'foundation': 'Foundation',
'semantic-ui': 'Semantic UI',
'material-ui': 'Material UI',
'@chakra-ui/core': 'Chakra UI',
}
detected_frameworks = []
if package_json.exists():
with open(package_json) as f:
package_data = json.load(f)
deps = {**package_data.get('dependencies', {}), **package_data.get('devDependencies', {})}
for pkg, name in css_frameworks.items():
if pkg in deps:
detected_frameworks.append({
'name': name,
'package': pkg,
'version': deps[pkg]
})
self.detected['css_framework'] = {
'frameworks': detected_frameworks,
'primary': detected_frameworks[0]['name'] if detected_frameworks else 'Custom CSS',
'detected': len(detected_frameworks) > 0
}
if detected_frameworks:
print(f" ✓ CSS Framework: {detected_frameworks[0]['name']}")
else:
print(f" ✓ CSS: Custom/Inline")
def detect_tailwind_version(self):
"""Detect Tailwind CSS version."""
package_json = self.website_path / 'package.json'
tailwind_config = self.website_path / 'tailwind.config.js'
tailwind_config_ts = self.website_path / 'tailwind.config.ts'
if package_json.exists():
with open(package_json) as f:
package_data = json.load(f)
deps = {**package_data.get('dependencies', {}), **package_data.get('devDependencies', {})}
if 'tailwindcss' in deps:
version = deps['tailwindcss']
major_version = version.replace('^', '').replace('~', '').split('.')[0]
# Check for v4 features
has_v4_features = False
if tailwind_config.exists():
with open(tailwind_config) as f:
config = f.read()
# v4 uses different config format
has_v4_features = '@theme' in config or 'import theme' in config
self.detected['tailwind'] = {
'version': version,
'major_version': int(major_version) if major_version.isdigit() else 3,
'config_file': 'tailwind.config.js' if tailwind_config.exists() else 'tailwind.config.ts' if tailwind_config_ts.exists() else None,
'needs_upgrade': int(major_version) < 4 if major_version.isdigit() else False,
'detected': True
}
print(f" ✓ Tailwind CSS v{major_version}: {'Needs upgrade to v4' if int(major_version) < 4 else 'Up to date'}")
def detect_pages_structure(self):
"""Detect pages structure."""
pages_dir = self.website_path / 'src' / 'pages'
if pages_dir.exists():
pages = list(pages_dir.glob('**/*.astro'))
pages.extend(list(pages_dir.glob('**/*.md')))
pages.extend(list(pages_dir.glob('**/*.mdx')))
self.detected['pages'] = {
'count': len(pages),
'structure': 'flat' if len(list(pages_dir.glob('*.astro'))) > len(pages) / 2 else 'nested',
'has_i18n': any('/th/' in str(p) or '(th)' in str(p) for p in pages),
'detected': True
}
print(f" ✓ Pages: {len(pages)} pages detected")
def detect_content_collections(self):
"""Detect Astro Content Collections."""
content_dir = self.website_path / 'src' / 'content'
content_config = self.website_path / 'src' / 'content.config.ts'
collections = []
if content_dir.exists():
for subdir in content_dir.iterdir():
if subdir.is_dir() and not subdir.name.startswith('_'):
collection_files = list(subdir.glob('*.md')) + list(subdir.glob('*.mdx'))
if collection_files:
collections.append({
'name': subdir.name,
'file_count': len(collection_files)
})
self.detected['content_collections'] = {
'collections': collections,
'has_config': content_config.exists(),
'detected': len(collections) > 0
}
if collections:
print(f" ✓ Content Collections: {len(collections)} collections")
def detect_integrations(self):
"""Detect Astro integrations."""
astro_config = self.website_path / 'astro.config.mjs'
astro_config_ts = self.website_path / 'astro.config.ts'
config_file = astro_config if astro_config.exists() else astro_config_ts if astro_config_ts.exists() else None
integrations = []
if config_file:
with open(config_file) as f:
config_content = f.read()
# Detect common integrations
integration_patterns = {
'tailwind': 'tailwind()',
'react': 'react()',
'vue': 'vue()',
'svelte': 'svelte()',
'solid': 'solid()',
'mdx': 'mdx()',
'sitemap': 'sitemap()',
'vercel': 'vercel()',
'netlify': 'netlify()',
'node': 'node()',
'static-adapter': 'staticAdapter',
}
for name, pattern in integration_patterns.items():
if pattern in config_content:
integrations.append(name)
self.detected['integrations'] = {
'integrations': integrations,
'config_file': config_file.name if config_file else None,
'detected': len(integrations) > 0
}
if integrations:
print(f" ✓ Integrations: {', '.join(integrations)}")
def detect_custom_css(self):
"""Detect custom CSS files and inline styles."""
src_dir = self.website_path / 'src'
css_files = []
inline_styles = 0
if src_dir.exists():
# Find CSS files
for css_file in src_dir.glob('**/*.css'):
css_files.append(str(css_file.relative_to(self.website_path)))
# Count inline styles in Astro files
for astro_file in src_dir.glob('**/*.astro'):
with open(astro_file) as f:
content = f.read()
# Count style tags
inline_styles += content.count('<style>')
self.detected['custom_css'] = {
'css_files': css_files,
'inline_style_count': inline_styles,
'detected': len(css_files) > 0 or inline_styles > 0
}
print(f" ✓ Custom CSS: {len(css_files)} files, {inline_styles} inline styles")
class MigrationPlanner:
"""Create detailed migration plan."""
def __init__(self, tech_stack: Dict[str, Any], input_path: Path, output_path: Path):
self.tech_stack = tech_stack
self.input_path = input_path
self.output_path = output_path
self.plan = {}
def create_plan(self) -> Dict[str, Any]:
"""Create comprehensive migration plan."""
print("\n📋 Creating migration plan...\n")
self.plan['summary'] = self._create_summary()
self.plan['preservation'] = self._plan_preservation()
self.plan['css_conversion'] = self._plan_css_conversion()
self.plan['rebuild'] = self._plan_rebuild()
self.plan['enhancements'] = self._plan_enhancements()
self.plan['testing'] = self._plan_testing()
self.plan['risks'] = self._identify_risks()
return self.plan
def _create_summary(self) -> Dict[str, Any]:
"""Create migration summary."""
astro_version = self.tech_stack.get('astro', {}).get('version', 'unknown')
css_framework = self.tech_stack.get('css_framework', {}).get('primary', 'Unknown')
tailwind_version = self.tech_stack.get('tailwind', {}).get('major_version', 0)
page_count = self.tech_stack.get('pages', {}).get('count', 0)
return {
'source_astro_version': astro_version,
'target_astro_version': 'latest (5.x)',
'css_framework': css_framework,
'tailwind_upgrade': f"v{tailwind_version} → v4" if tailwind_version < 4 else "No upgrade needed",
'page_count': page_count,
'estimated_time': f"{max(10, page_count * 2)} minutes"
}
def _plan_preservation(self) -> Dict[str, Any]:
"""Plan content preservation."""
return {
'steps': [
'Extract all inline CSS from .astro files',
'Extract all page content (frontmatter + body)',
'Copy all static assets (public/ folder)',
'Copy all images and media files',
'Copy all content collections (blog, products, etc.)',
'Preserve all component logic and scripts',
'Keep all existing routes and URLs'
],
'preserved_exactly': [
'All page content (text, images, links)',
'All inline styles (<style> tags)',
'All component functionality',
'All existing URLs and routes',
'All metadata (title, description, etc.)'
]
}
def _plan_css_conversion(self) -> Dict[str, Any]:
"""Plan CSS framework conversion."""
tailwind = self.tech_stack.get('tailwind', {})
needs_upgrade = tailwind.get('needs_upgrade', False)
steps = []
if needs_upgrade:
steps.extend([
'Backup existing tailwind.config.js',
'Install Tailwind CSS v4',
'Convert tailwind.config.js to v4 format',
'Update CSS imports to v4 syntax',
'Test all pages for CSS issues',
'Fix any breaking changes'
])
else:
steps.append('No CSS framework upgrade needed')
return {
'needs_conversion': needs_upgrade,
'steps': steps,
'breaking_changes': [
'Tailwind v4 uses different config format',
'Some utilities may have changed',
'Custom CSS may need adjustment'
] if needs_upgrade else []
}
def _plan_rebuild(self) -> Dict[str, Any]:
"""Plan Astro rebuild."""
return {
'steps': [
'Create fresh Astro 5.x project',
'Install all required integrations',
'Migrate preserved content to new structure',
'Apply CSS conversions',
'Update Astro config for new features',
'Add new components (cookie consent, etc.)'
],
'fresh_install': True,
'keep_existing_components': True
}
def _plan_enhancements(self) -> Dict[str, Any]:
"""Plan new features to add."""
return {
'new_features': [
'PDPA-compliant Privacy Policy (Thai law)',
'PDPA-compliant Terms of Service (Thai law)',
'Working cookie consent (blocks cookies until consent)',
'Consent logging database',
'Umami Analytics integration',
'i18n routing (Thai/English)',
'Admin dashboard for consent logs'
],
'optional_features': [
'Blog post templates',
'Product pages',
'Contact forms',
'SEO optimization'
]
}
def _plan_testing(self) -> Dict[str, Any]:
"""Plan testing steps."""
return {
'pre_deploy_tests': [
'Docker build completes successfully',
'All pages load without errors',
'All inline CSS renders correctly',
'Cookie consent blocks cookies until accepted',
'All links work',
'Mobile responsive design works',
'Backend functions work (forms, databases)',
'Analytics tracking works (if consented)'
],
'manual_verification': [
'Compare migrated pages with originals',
'Verify all content is preserved',
'Test cookie consent functionality',
'Test on multiple browsers',
'Test on mobile devices'
]
}
def _identify_risks(self) -> List[Dict[str, str]]:
"""Identify potential risks."""
risks = []
if self.tech_stack.get('astro', {}).get('version', 'unknown') == 'unknown':
risks.append({
'risk': 'Astro version unknown',
'impact': 'Migration may require manual adjustments',
'mitigation': 'Manual review of package.json required'
})
inline_styles = self.tech_stack.get('custom_css', {}).get('inline_style_count', 0)
if inline_styles > 50:
risks.append({
'risk': f'High inline CSS count ({inline_styles} styles)',
'impact': 'May take longer to verify all styles',
'mitigation': 'Automated CSS extraction and verification'
})
tailwind = self.tech_stack.get('tailwind', {})
if tailwind.get('needs_upgrade', False):
risks.append({
'risk': 'Tailwind v3 → v4 upgrade',
'impact': 'Some CSS utilities may break',
'mitigation': 'Thorough CSS testing on all pages'
})
return risks
def main():
parser = argparse.ArgumentParser(
description='Smart Website Migration - Detect, Plan, then Migrate'
)
parser.add_argument('--input', '-i', required=True, help='Input directory (existing website)')
parser.add_argument('--output', '-o', required=True, help='Output directory (migrated website)')
parser.add_argument('--plan-only', action='store_true', help='Only create plan, don\'t migrate')
parser.add_argument('--languages', default='th,en', help='Languages (comma-separated)')
args = parser.parse_args()
input_path = Path(args.input)
output_path = Path(args.output)
if not input_path.exists():
print(f"❌ Error: Input directory '{input_path}' does not exist")
sys.exit(1)
print("=" * 70)
print("🔄 SMART WEBSITE MIGRATION")
print("=" * 70)
print(f"\n📁 Input: {input_path}")
print(f"📁 Output: {output_path}")
print(f"📋 Plan only: {args.plan_only}")
print()
# Step 1: Detect tech stack
detector = TechStackDetector(input_path)
tech_stack = detector.detect_all()
# Step 2: Create migration plan
planner = MigrationPlanner(tech_stack, input_path, output_path)
plan = planner.create_plan()
# Save plan to file
plan_file = output_path.parent / f"migration_plan_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
output_path.mkdir(parents=True, exist_ok=True)
with open(plan_file, 'w') as f:
json.dump({
'tech_stack': tech_stack,
'migration_plan': plan,
'created_at': datetime.now().isoformat()
}, f, indent=2)
print(f"\n📄 Migration plan saved to: {plan_file}")
# Print plan summary
print("\n" + "=" * 70)
print("📋 MIGRATION PLAN SUMMARY")
print("=" * 70)
summary = plan.get('summary', {})
print(f"\n📊 Summary:")
print(f" • Astro: {summary.get('source_astro_version', 'unknown')}{summary.get('target_astro_version', 'latest')}")
print(f" • CSS: {summary.get('css_framework', 'Unknown')}")
print(f" • Tailwind: {summary.get('tailwind_upgrade', 'N/A')}")
print(f" • Pages: {summary.get('page_count', 0)} pages")
print(f" • Estimated time: {summary.get('estimated_time', 'unknown')}")
# Print risks
risks = plan.get('risks', [])
if risks:
print(f"\n⚠️ Risks identified: {len(risks)}")
for risk in risks:
print(f"{risk['risk']}")
print(f" Impact: {risk['impact']}")
print(f" Mitigation: {risk['mitigation']}")
if args.plan_only:
print("\n✅ Plan created successfully!")
print("\nTo proceed with migration, run:")
print(f" python3 migrate_existing_website.py \\")
print(f" --input '{input_path}' \\")
print(f" --output '{output_path}'")
else:
print("\n⚠️ WARNING: Full migration not yet implemented!")
print("\nThis is a safety measure. The migration script will:")
print(" 1. Review this plan carefully")
print(" 2. Manually verify all detected tech stack")
print(" 3. Approve the migration plan")
print(" 4. Then we'll implement the full migration logic")
print("\nPlease review the plan and let us know if you want to proceed!")
print("\n" + "=" * 70)
if __name__ == '__main__':
main()