UNPKG

figma-restoration-mcp-vue-tools

Version:

Professional Figma Component Restoration Kit - MCP tools with snapDOM-powered high-quality screenshots, intelligent shadow detection, and advanced diff analysis for Vue component restoration. Features enhanced figma_compare with color-coded region analysi

234 lines (211 loc) 7.53 kB
import fs from 'fs/promises'; import path from 'path'; import chalk from 'chalk'; import { TimeoutManager } from '../utils/timeout-manager.js'; import { ErrorHandler, ValidationError, FormatError } from '../utils/error-handler.js'; /** * BaseProcessor - Abstract base class for image format processors */ export class BaseProcessor { constructor(format) { if (new.target === BaseProcessor) { throw new Error('BaseProcessor is an abstract class and cannot be instantiated directly'); } this.format = format; } /** * Optimize an image file (abstract method) * @param {string} inputPath - Path to input file * @param {Object} config - Optimization configuration * @returns {Promise<Object>} - Optimization result */ async optimize(inputPath, config = {}) { throw new Error('optimize() method must be implemented by subclasses'); } /** * Validate that the file format matches this processor * @param {string} filePath - Path to the file * @returns {Promise<boolean>} - True if format is supported */ async validateFormat(filePath) { const detectedFormat = this.detectFormat(filePath); if (detectedFormat !== this.format) { throw new FormatError( `Format mismatch: expected ${this.format}, detected ${detectedFormat}`, detectedFormat ); } return true; } /** * Detect image format from file extension * @param {string} filePath - Path to the file * @returns {string} - Detected format */ detectFormat(filePath) { const ext = path.extname(filePath).toLowerCase(); switch (ext) { case '.png': return 'png'; case '.jpg': case '.jpeg': return 'jpeg'; case '.svg': return 'svg'; case '.webp': return 'webp'; default: throw new FormatError(`Unsupported image format: ${ext}. Supported formats: .png, .jpg, .jpeg, .svg, .webp`); } } /** * Get default configuration for this processor * @returns {Object} - Default configuration */ getDefaultConfig() { return {}; } /** * Merge user configuration with defaults * @param {Object} userConfig - User-provided configuration * @returns {Object} - Merged configuration */ mergeConfig(userConfig = {}) { return { ...this.getDefaultConfig(), ...userConfig }; } /** * Validate configuration parameters * @param {Object} config - Configuration to validate * @returns {Object} - Validated configuration */ validateConfig(config) { // Base validation - subclasses can override if (config.timeout && (typeof config.timeout !== 'number' || config.timeout <= 0)) { throw new ValidationError('Timeout must be a positive number', 'timeout'); } return config; } /** * Get file statistics * @param {string} filePath - Path to the file * @returns {Promise<Object>} - File statistics */ async getFileStats(filePath) { try { const stats = await fs.stat(filePath); return { size: stats.size, modified: stats.mtime, created: stats.birthtime }; } catch (error) { throw new Error(`Failed to get file statistics: ${error.message}`); } } /** * Create a backup of the original file * @param {string} filePath - Path to the file * @returns {Promise<string>} - Path to backup file */ async createBackup(filePath) { const backupPath = `${filePath}.backup.${Date.now()}`; try { await fs.copyFile(filePath, backupPath); console.log(chalk.blue(`📋 Created backup: ${path.basename(backupPath)}`)); return backupPath; } catch (error) { throw new Error(`Failed to create backup: ${error.message}`); } } /** * Safely replace the original file with optimized version * @param {string} originalPath - Path to original file * @param {string} optimizedPath - Path to optimized file * @param {boolean} createBackup - Whether to create a backup * @returns {Promise<void>} */ async replaceFile(originalPath, optimizedPath, createBackup = false) { try { let backupPath = null; if (createBackup) { backupPath = await this.createBackup(originalPath); } // Atomic replacement using rename await fs.rename(optimizedPath, originalPath); console.log(chalk.green(`✅ File replaced: ${path.basename(originalPath)}`)); // Clean up backup if replacement was successful and not requested if (backupPath && !createBackup) { await fs.unlink(backupPath).catch(() => { // Ignore cleanup errors }); } } catch (error) { throw new Error(`Failed to replace file: ${error.message}`); } } /** * Format file size in human-readable format * @param {number} bytes - File size in bytes * @returns {string} - Formatted file size */ formatFileSize(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Calculate compression statistics * @param {number} originalSize - Original file size in bytes * @param {number} optimizedSize - Optimized file size in bytes * @returns {Object} - Compression statistics */ calculateCompressionStats(originalSize, optimizedSize) { const reduction = originalSize - optimizedSize; const reductionPercentage = originalSize > 0 ? ((reduction / originalSize) * 100) : 0; return { originalSize, optimizedSize, reduction, reductionPercentage: parseFloat(reductionPercentage.toFixed(2)), compressionRatio: originalSize > 0 ? parseFloat((optimizedSize / originalSize).toFixed(3)) : 1 }; } /** * Log optimization results * @param {Object} stats - Compression statistics * @param {string} operation - Operation name */ logOptimizationResults(stats, operation = 'Optimization') { console.log(chalk.green(`✅ ${operation} completed successfully!`)); console.log(chalk.yellow(`📊 ${operation} Results:`)); console.log(chalk.gray(` Original size: ${this.formatFileSize(stats.originalSize)}`)); console.log(chalk.gray(` Optimized size: ${this.formatFileSize(stats.optimizedSize)}`)); console.log(chalk.gray(` Size reduction: ${this.formatFileSize(stats.reduction)} (${stats.reductionPercentage}%)`)); console.log(chalk.gray(` Compression ratio: ${stats.compressionRatio}:1`)); } /** * Execute optimization with timeout and error handling * @param {Function} optimizationFn - Function that performs the optimization * @param {Object} config - Configuration including timeout settings * @returns {Promise<Object>} - Optimization result */ async executeWithTimeout(optimizationFn, config = {}) { const timeoutConfig = TimeoutManager.createConfig('processing', { timeoutMs: (config.timeout || 30) * 1000, operationName: `${this.format.toUpperCase()} optimization`, cleanupFn: config.cleanupFn }); try { return await TimeoutManager.withTimeout(optimizationFn, timeoutConfig); } catch (error) { const errorInfo = ErrorHandler.categorizeError(error, { processor: this.format, operation: 'optimization' }); ErrorHandler.logError(errorInfo, `${this.format.toUpperCase()} Processor`); throw error; } } }