#!/usr/bin/env node /** * Design System Migration Script * * Migrates existing theme classes to new design system classes * Replaces hardcoded colors with semantic design system alternatives */ const fs = require('fs'); const path = require('path'); // Comprehensive class mappings from old theme/hardcoded classes to design system const DESIGN_SYSTEM_REPLACEMENTS = { // Background replacements 'theme-bg': 'ds-bg', 'theme-bg-secondary': 'ds-bg-secondary', 'theme-bg-page': 'ds-bg-page', 'bg-white dark:bg-gray-900': 'ds-bg', 'bg-gray-50 dark:bg-gray-900': 'ds-bg-page', 'bg-gray-100 dark:bg-gray-800': 'ds-bg-secondary', // Text replacements 'theme-text': 'ds-text', 'theme-text-secondary': 'ds-text-secondary', 'theme-text-muted': 'ds-text-muted', 'text-gray-900 dark:text-gray-100': 'ds-text', 'text-gray-600 dark:text-gray-400': 'ds-text-secondary', 'text-gray-500 dark:text-gray-400': 'ds-text-muted', 'text-gray-500': 'ds-text-muted', // Button replacements 'theme-toggle': 'ds-theme-toggle', 'btn-primary': 'ds-btn-primary', 'btn-secondary': 'ds-btn-secondary', 'btn-ghost': 'ds-btn-ghost', // Navigation replacements 'nav-link': 'ds-nav-link', 'nav-link-active': 'ds-nav-link-active', // Card replacements 'theme-card': 'ds-card', 'card-base': 'ds-card', 'card-hover': 'ds-card-hover', // Border replacements 'theme-border': 'ds-border', 'border-gray-200 dark:border-gray-700': 'ds-border', 'border-gray-300 dark:border-gray-600': 'ds-border', // Typography scale replacements 'text-4xl font-semibold tracking-tight': 'ds-heading-1', 'text-3xl font-semibold': 'ds-heading-2', 'text-2xl font-semibold': 'ds-heading-3', 'text-xl font-semibold': 'ds-heading-4', 'text-lg': 'ds-body', 'text-sm': 'ds-caption', // Hardcoded color replacements 'bg-blue-200': 'bg-ds-brand-primary/20', 'bg-blue-600': 'bg-ds-brand-primary', 'bg-green-200': 'bg-ds-success/20', 'bg-green-600': 'bg-ds-success', 'bg-purple-500': 'bg-ds-brand-secondary', 'bg-pink-500': 'bg-ds-brand-accent', 'text-blue-600 dark:text-blue-400': 'text-ds-brand-primary', 'text-green-600 dark:text-green-400': 'text-ds-success', 'text-gray-800 dark:text-gray-200': 'ds-text', // Shadow replacements 'shadow-sm': 'ds-shadow-sm', 'shadow-md': 'ds-shadow-md', 'shadow-lg': 'ds-shadow-lg', // Spacing replacements with container 'mx-auto max-w-4xl py-16 sm:py-24': 'ds-container-md py-ds-16 sm:py-ds-24', 'mx-auto max-w-2xl': 'ds-container-sm', 'mx-auto max-w-6xl': 'ds-container-lg', 'px-6 lg:px-8': 'ds-container', // Grid and layout 'grid grid-cols-1 md:grid-cols-2 gap-8': 'grid grid-cols-1 md:grid-cols-2 gap-ds-8', 'space-y-2': 'space-y-ds-2', 'space-x-2': 'space-x-ds-2', 'flex space-x-2': 'flex space-x-ds-2', 'mb-4': 'mb-ds-4', 'mb-12': 'mb-ds-12', 'mt-6': 'mt-ds-6', 'mt-12': 'mt-ds-12', 'p-6': 'p-ds-6', 'p-2': 'p-ds-2', 'px-6 py-3': 'px-ds-6 py-ds-3', 'px-4 py-2': 'px-ds-4 py-ds-2', // Rounded corners 'rounded-lg': 'ds-rounded-lg', 'rounded-full': 'ds-rounded-full', 'rounded': 'ds-rounded', }; // Individual class replacements for when full context patterns don't match const INDIVIDUAL_CLASS_REPLACEMENTS = { // Hardcoded grays to design system neutrals 'text-gray-900': 'text-ds-neutral-900', 'text-gray-800': 'text-ds-neutral-800', 'text-gray-700': 'text-ds-neutral-700', 'text-gray-600': 'text-ds-neutral-600', 'text-gray-500': 'text-ds-neutral-500', 'text-gray-400': 'text-ds-neutral-400', 'text-gray-300': 'text-ds-neutral-300', 'text-gray-200': 'text-ds-neutral-200', 'text-gray-100': 'text-ds-neutral-100', 'bg-gray-900': 'bg-ds-neutral-900', 'bg-gray-800': 'bg-ds-neutral-800', 'bg-gray-700': 'bg-ds-neutral-700', 'bg-gray-600': 'bg-ds-neutral-600', 'bg-gray-500': 'bg-ds-neutral-500', 'bg-gray-400': 'bg-ds-neutral-400', 'bg-gray-300': 'bg-ds-neutral-300', 'bg-gray-200': 'bg-ds-neutral-200', 'bg-gray-100': 'bg-ds-neutral-100', 'bg-gray-50': 'bg-ds-neutral-50', 'border-gray-900': 'border-ds-neutral-900', 'border-gray-800': 'border-ds-neutral-800', 'border-gray-700': 'border-ds-neutral-700', 'border-gray-600': 'border-ds-neutral-600', 'border-gray-500': 'border-ds-neutral-500', 'border-gray-400': 'border-ds-neutral-400', 'border-gray-300': 'border-ds-neutral-300', 'border-gray-200': 'border-ds-neutral-200', 'border-gray-100': 'border-ds-neutral-100', // Legacy theme classes to design system 'theme-text': 'ds-text', 'theme-text-secondary': 'ds-text-secondary', 'theme-text-muted': 'ds-text-muted', 'theme-bg': 'ds-bg', 'theme-bg-secondary': 'ds-bg-secondary', 'theme-bg-page': 'ds-bg-page', 'theme-border': 'ds-border', 'theme-card': 'ds-card', 'theme-toggle': 'ds-theme-toggle', 'nav-link': 'ds-nav-link', // Brand colors 'bg-blue-500': 'bg-ds-brand-primary', 'bg-blue-600': 'bg-ds-brand-primary', 'text-blue-500': 'text-ds-brand-primary', 'text-blue-600': 'text-ds-brand-primary', 'border-blue-500': 'border-ds-brand-primary', // Success colors 'bg-green-500': 'bg-ds-success', 'bg-green-600': 'bg-ds-success', 'text-green-500': 'text-ds-success', 'text-green-600': 'text-ds-success', // Error colors 'bg-red-500': 'bg-ds-error', 'bg-red-600': 'bg-ds-error', 'text-red-500': 'text-ds-error', 'text-red-600': 'text-ds-error', // Warning colors 'bg-yellow-500': 'bg-ds-warning', 'bg-amber-500': 'bg-ds-warning', 'text-yellow-500': 'text-ds-warning', 'text-amber-500': 'text-ds-warning', }; function findRustFiles(dir) { const files = []; function walk(currentPath) { const items = fs.readdirSync(currentPath); for (const item of items) { const fullPath = path.join(currentPath, item); const stat = fs.statSync(fullPath); if (stat.isDirectory() && !item.startsWith('.') && item !== 'target') { walk(fullPath); } else if (item.endsWith('.rs')) { files.push(fullPath); } } } walk(dir); return files; } function migrateFileToDesignSystem(filePath, dryRun = false) { console.log(`šŸŽØ ${dryRun ? 'Analyzing' : 'Migrating'}: ${filePath}`); let content = fs.readFileSync(filePath, 'utf8'); let changes = 0; // Apply complex pattern replacements first (order matters) const sortedReplacements = Object.entries(DESIGN_SYSTEM_REPLACEMENTS) .sort(([a], [b]) => b.length - a.length); // Longer patterns first for (const [pattern, replacement] of sortedReplacements) { const regex = new RegExp(pattern.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'); const matches = content.match(regex); if (matches) { console.log(` šŸ“ ${matches.length}x: ${pattern} → ${replacement}`); content = content.replace(regex, replacement); changes += matches.length; } } // Apply individual class replacements for (const [oldClass, newClass] of Object.entries(INDIVIDUAL_CLASS_REPLACEMENTS)) { // Use word boundaries to avoid partial matches const regex = new RegExp(`\\\\b${oldClass.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\\\b`, 'g'); const matches = content.match(regex); if (matches) { console.log(` šŸ”„ ${matches.length}x: ${oldClass} → ${newClass}`); content = content.replace(regex, newClass); changes += matches.length; } } if (!dryRun && changes > 0) { fs.writeFileSync(filePath, content); console.log(`āœ… Applied ${changes} changes to ${filePath}`); } else if (changes > 0) { console.log(`šŸ“‹ Would apply ${changes} changes (use --apply to execute)`); } else { console.log(`✨ No design system changes needed`); } return changes; } function migrateAllFiles(clientDir, dryRun = false) { console.log('šŸŽØ Migrating all files to design system...\\n'); const files = findRustFiles(clientDir); let totalChanges = 0; let filesChanged = 0; for (const file of files) { const changes = migrateFileToDesignSystem(file, dryRun); if (changes > 0) { totalChanges += changes; filesChanged++; } console.log(''); // Add spacing between files } console.log(`šŸ“Š Migration Summary:`); console.log(` Files processed: ${files.length}`); console.log(` Files ${dryRun ? 'needing changes' : 'changed'}: ${filesChanged}`); console.log(` Total class migrations: ${totalChanges}`); if (totalChanges > 0) { console.log('\\nšŸ’” Next Steps:'); console.log('1. Build CSS: node scripts/build/build-design-system.js'); console.log('2. Test in browser: just dev'); console.log('3. Verify themes work: check light/dark mode'); console.log('4. Test responsive: check mobile/tablet/desktop'); } return totalChanges; } // CLI handling if (require.main === module) { const args = process.argv.slice(2); const command = args[0]; if (command === 'file') { const filePath = args[1]; const dryRun = !args.includes('--apply'); if (!filePath) { console.error('Usage: migrate-to-design-system.js file [--apply]'); process.exit(1); } migrateFileToDesignSystem(filePath, dryRun); } else if (command === 'all' || !command) { const clientDir = path.join(__dirname, '..', 'crates', 'client'); const dryRun = !args.includes('--apply'); migrateAllFiles(clientDir, dryRun); } else { console.log('Usage:'); console.log(' node migrate-to-design-system.js all [--apply] # Migrate all files'); console.log(' node migrate-to-design-system.js file [--apply] # Migrate single file'); console.log(''); console.log('Examples:'); console.log(' node migrate-to-design-system.js all # Dry run all files'); console.log(' node migrate-to-design-system.js all --apply # Migrate all files'); console.log(' node migrate-to-design-system.js file src/pages/Home.rs # Dry run single file'); } } module.exports = { migrateFileToDesignSystem, migrateAllFiles, DESIGN_SYSTEM_REPLACEMENTS };