website-htmx-rustelo-code/scripts/migrations/migrate-to-design-system.js

306 lines
9.9 KiB
JavaScript
Raw Normal View History

2026-07-10 03:44:13 +01:00
#!/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 <path> [--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 <path> [--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 };