import { readFileSync } from 'fs';
import { validateDesignImplementation } from './figma.js';
import type { ValidationReport, ValidationResult } from '../figma/validator.js';
export interface AutoFixInput {
figmaUrl?: string;
fileKey?: string;
nodeId?: string;
figmaToken: string;
componentFile: string;
tolerance?: number;
autoApply?: boolean;
}
export interface FixSuggestion {
property: string;
currentValue: string | null;
expectedValue: string | null;
tailwindFix?: string;
styledFix?: string;
cssModuleFix?: string;
searchPattern?: string;
replacePattern?: string;
priority: 'high' | 'medium' | 'low';
description: string;
}
export interface AutoFixResult {
success: boolean;
validationScore: number;
needsFix: boolean;
fixes: FixSuggestion[];
agentPrompt: string;
componentFile: string;
error?: string;
}
// Mapping from Figma values to Tailwind classes
const TAILWIND_SPACING_MAP: Record<number, string> = {
0: '0', 2: '0.5', 4: '1', 6: '1.5', 8: '2', 10: '2.5', 12: '3', 14: '3.5',
16: '4', 20: '5', 24: '6', 28: '7', 32: '8', 36: '9', 40: '10', 44: '11',
48: '12', 56: '14', 64: '16', 80: '20', 96: '24', 112: '28', 128: '32'
};
const TAILWIND_FONT_SIZE_MAP: Record<number, string> = {
12: 'text-xs', 14: 'text-sm', 16: 'text-base', 18: 'text-lg', 20: 'text-xl',
24: 'text-2xl', 30: 'text-3xl', 36: 'text-4xl', 48: 'text-5xl', 60: 'text-6xl'
};
const TAILWIND_FONT_WEIGHT_MAP: Record<number, string> = {
100: 'font-thin', 200: 'font-extralight', 300: 'font-light', 400: 'font-normal',
500: 'font-medium', 600: 'font-semibold', 700: 'font-bold', 800: 'font-extrabold', 900: 'font-black'
};
const TAILWIND_COLOR_MAP: Record<string, string> = {
'#FFFFFF': 'white', '#000000': 'black',
'#F9FAFB': 'gray-50', '#F3F4F6': 'gray-100', '#E5E7EB': 'gray-200',
'#D1D5DB': 'gray-300', '#9CA3AF': 'gray-400', '#6B7280': 'gray-500',
'#4B5563': 'gray-600', '#374151': 'gray-700', '#1F2937': 'gray-800', '#111827': 'gray-900',
'#EF4444': 'red-500', '#DC2626': 'red-600',
'#22C55E': 'green-500', '#16A34A': 'green-600',
'#3B82F6': 'blue-500', '#2563EB': 'blue-600',
'#EAB308': 'yellow-500', '#CA8A04': 'yellow-600',
'#A855F7': 'purple-500', '#9333EA': 'purple-600',
'#EC4899': 'pink-500', '#DB2777': 'pink-600',
'#6366F1': 'indigo-500', '#4F46E5': 'indigo-600'
};
function findClosestSpacing(value: number): string {
const keys = Object.keys(TAILWIND_SPACING_MAP).map(Number);
const closest = keys.reduce((prev, curr) =>
Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
);
return TAILWIND_SPACING_MAP[closest];
}
function findClosestFontSize(value: number): string {
const keys = Object.keys(TAILWIND_FONT_SIZE_MAP).map(Number);
const closest = keys.reduce((prev, curr) =>
Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
);
return TAILWIND_FONT_SIZE_MAP[closest];
}
function findClosestColor(hex: string): string {
// Direct match
if (TAILWIND_COLOR_MAP[hex.toUpperCase()]) {
return TAILWIND_COLOR_MAP[hex.toUpperCase()];
}
// Return arbitrary value if no match
return `[${hex}]`;
}
function generateFixSuggestion(validation: ValidationResult): FixSuggestion | null {
if (validation.status === 'match') return null;
const figmaValue = validation.figmaValue;
if (figmaValue === null) return null;
const suggestion: FixSuggestion = {
property: validation.property,
currentValue: validation.implementedValue as string | null,
expectedValue: figmaValue as string,
priority: validation.status === 'missing' ? 'high' : 'medium',
description: ''
};
// Generate Tailwind fixes based on property type
switch (validation.property) {
case 'padding-top':
case 'padding-right':
case 'padding-bottom':
case 'padding-left': {
const px = parseInt(String(figmaValue));
const twValue = findClosestSpacing(px);
const prefix = validation.property.replace('padding-', 'p');
const prefixMap: Record<string, string> = {
'pt': 'pt', 'pr': 'pr', 'pb': 'pb', 'pl': 'pl'
};
suggestion.tailwindFix = `${prefixMap[prefix]}-${twValue}`;
suggestion.styledFix = `${validation.property}: ${figmaValue};`;
suggestion.description = `Change ${validation.property} to ${figmaValue}`;
break;
}
case 'gap': {
const px = parseInt(String(figmaValue));
const twValue = findClosestSpacing(px);
suggestion.tailwindFix = `gap-${twValue}`;
suggestion.styledFix = `gap: ${figmaValue};`;
suggestion.description = `Change gap to ${figmaValue}`;
break;
}
case 'background': {
const color = findClosestColor(String(figmaValue));
suggestion.tailwindFix = `bg-${color}`;
suggestion.styledFix = `background-color: ${figmaValue};`;
suggestion.description = `Change background color to ${figmaValue}`;
break;
}
case 'text-color': {
const color = findClosestColor(String(figmaValue));
suggestion.tailwindFix = `text-${color}`;
suggestion.styledFix = `color: ${figmaValue};`;
suggestion.description = `Change text color to ${figmaValue}`;
break;
}
case 'font-size': {
const px = parseInt(String(figmaValue));
suggestion.tailwindFix = findClosestFontSize(px);
suggestion.styledFix = `font-size: ${figmaValue};`;
suggestion.description = `Change font size to ${figmaValue}`;
break;
}
case 'font-weight': {
const weight = parseInt(String(figmaValue));
suggestion.tailwindFix = TAILWIND_FONT_WEIGHT_MAP[weight] || `font-[${weight}]`;
suggestion.styledFix = `font-weight: ${figmaValue};`;
suggestion.description = `Change font weight to ${figmaValue}`;
break;
}
case 'border-radius': {
const px = parseInt(String(figmaValue));
const radiusMap: Record<number, string> = {
0: 'rounded-none', 2: 'rounded-sm', 4: 'rounded', 6: 'rounded-md',
8: 'rounded-lg', 12: 'rounded-xl', 16: 'rounded-2xl', 24: 'rounded-3xl'
};
const closest = Object.keys(radiusMap).map(Number).reduce((prev, curr) =>
Math.abs(curr - px) < Math.abs(prev - px) ? curr : prev
);
suggestion.tailwindFix = radiusMap[closest];
suggestion.styledFix = `border-radius: ${figmaValue};`;
suggestion.description = `Change border radius to ${figmaValue}`;
break;
}
default:
suggestion.description = `Update ${validation.property} from ${validation.implementedValue} to ${figmaValue}`;
}
return suggestion;
}
function generateAgentPrompt(
componentFile: string,
fixes: FixSuggestion[],
validationScore: number
): string {
if (fixes.length === 0) {
return `✅ Component ${componentFile} matches Figma design with ${validationScore}% accuracy. No fixes needed.`;
}
const lines: string[] = [];
lines.push(`🔧 **AUTO-FIX REQUIRED** for \`${componentFile}\``);
lines.push('');
lines.push(`Current design compliance: **${validationScore}%**`);
lines.push('');
lines.push('## Required Changes');
lines.push('');
// Group by priority
const highPriority = fixes.filter(f => f.priority === 'high');
const mediumPriority = fixes.filter(f => f.priority === 'medium');
if (highPriority.length > 0) {
lines.push('### 🔴 Missing (High Priority)');
for (const fix of highPriority) {
lines.push(`- **${fix.property}**: Add \`${fix.tailwindFix || fix.styledFix}\``);
lines.push(` - Expected: \`${fix.expectedValue}\``);
}
lines.push('');
}
if (mediumPriority.length > 0) {
lines.push('### 🟡 Mismatches (Medium Priority)');
for (const fix of mediumPriority) {
lines.push(`- **${fix.property}**: Change to \`${fix.tailwindFix || fix.styledFix}\``);
lines.push(` - Current: \`${fix.currentValue}\``);
lines.push(` - Expected: \`${fix.expectedValue}\``);
}
lines.push('');
}
lines.push('## Instructions');
lines.push('');
lines.push(`1. Open \`${componentFile}\``);
lines.push('2. Find the className or styled-components styles');
lines.push('3. Apply the following Tailwind class changes:');
lines.push('');
const tailwindFixes = fixes.filter(f => f.tailwindFix).map(f => f.tailwindFix);
if (tailwindFixes.length > 0) {
lines.push('```');
lines.push(`Add/Replace: ${tailwindFixes.join(' ')}`);
lines.push('```');
}
lines.push('');
lines.push('4. Save the file');
lines.push('5. Run validation again to confirm compliance');
return lines.join('\n');
}
/**
* Validate component against Figma and generate fix suggestions
*/
export async function validateAndFix(input: AutoFixInput): Promise<AutoFixResult> {
try {
// Run validation
const validation = await validateDesignImplementation({
figmaUrl: input.figmaUrl,
fileKey: input.fileKey,
nodeId: input.nodeId,
figmaToken: input.figmaToken,
componentFile: input.componentFile,
tolerance: input.tolerance
});
if (!validation.success || !validation.report) {
return {
success: false,
validationScore: 0,
needsFix: false,
fixes: [],
agentPrompt: '',
componentFile: input.componentFile,
error: validation.error
};
}
// Generate fix suggestions for each issue
const fixes: FixSuggestion[] = [];
for (const v of validation.report.validations) {
const fix = generateFixSuggestion(v);
if (fix) {
fixes.push(fix);
}
}
const needsFix = fixes.length > 0;
const agentPrompt = generateAgentPrompt(
input.componentFile,
fixes,
validation.report.summary.score
);
return {
success: true,
validationScore: validation.report.summary.score,
needsFix,
fixes,
agentPrompt,
componentFile: input.componentFile
};
} catch (error) {
return {
success: false,
validationScore: 0,
needsFix: false,
fixes: [],
agentPrompt: '',
componentFile: input.componentFile,
error: error instanceof Error ? error.message : String(error)
};
}
}
/**
* Format the auto-fix result for MCP output
*/
export function formatAutoFixResult(result: AutoFixResult): string {
if (!result.success) {
return `Error: ${result.error}`;
}
return result.agentPrompt;
}