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

327 lines (286 loc) 10.3 kB
import fs from 'fs/promises'; import path from 'path'; import chalk from 'chalk'; import { ensureDirectory } from '../utils/path-config.js'; import { puppeteerManager } from '../utils/puppeteer-manager.js'; import { PuppeteerLaunchError, NetworkError, PermissionError, TimeoutError, MemoryError } from '../utils/puppeteer-errors.js'; export class SnapDOMScreenshotTool { constructor() { this.description = 'Take high-quality 3x scale screenshots using snapDOM technology for precise DOM-to-image capture'; this.DEFAULT_TIMEOUT = 3000; // 增加超时时间到15秒 this.inputSchema = { type: 'object', properties: { componentName: { type: 'string', description: 'Name of the component to screenshot' }, projectPath: { type: 'string', description: 'Path to the Vue project (required)' }, viewport: { type: 'object', properties: { width: { type: 'number', default: 1440 }, height: { type: 'number', default: 800 } }, description: 'Viewport size for screenshot' }, snapDOMOptions: { type: 'object', properties: { compress: { type: 'boolean', default: true }, fast: { type: 'boolean', default: false }, embedFonts: { type: 'boolean', default: true }, backgroundColor: { type: 'string', default: 'transparent' }, width: { type: 'number', description: 'Fixed width for output' }, height: { type: 'number', description: 'Fixed height for output' } }, description: 'snapDOM capture options for high-quality screenshots' }, outputPath: { type: 'string', description: 'Custom output path for screenshot (required)' }, selector: { type: 'string', description: 'Custom CSS selector to screenshot (optional)' } }, required: ['componentName', 'projectPath', 'outputPath'] }; } // 简化的超时包装函数 async withTimeout(promise, timeoutMs = 10000, errorMessage = 'Operation timed out') { return Promise.race([ promise, new Promise((_, reject) => setTimeout(() => reject(new Error(`${errorMessage} (${timeoutMs}ms)`)), timeoutMs) ) ]); } async execute(args) { // 验证必传参数 if (!args.componentName) { throw new Error('❌ 参数错误: componentName 是必传参数,请提供组件名称'); } if (!args.projectPath) { throw new Error('❌ 参数错误: projectPath 是必传参数,请提供项目路径'); } if (!args.outputPath) { throw new Error('❌ 参数错误: outputPath 是必传参数,请提供输出路径'); } // 验证项目路径是否存在 try { await fs.access(args.projectPath); } catch (error) { throw new Error(`❌ 项目路径不存在: ${args.projectPath}`); } // 验证输出路径的父目录是否存在,如果不存在则创建 const outputDir = path.dirname(args.outputPath); try { await fs.access(outputDir); } catch (error) { try { await fs.mkdir(outputDir, { recursive: true }); console.log(chalk.blue(`📁 创建输出目录: ${outputDir}`)); } catch (mkdirError) { throw new Error(`❌ 无法创建输出目录: ${outputDir} - ${mkdirError.message}`); } } const { componentName, projectPath, viewport = { width: 1440, height: 800 }, snapDOMOptions = { compress: true, fast: false, embedFonts: true, backgroundColor: 'transparent' }, outputPath, selector } = args; try { console.log(chalk.cyan('📸 snapDOM Screenshot Tool')); console.log(chalk.cyan(`Component: ${componentName}`)); console.log(chalk.gray('='.repeat(50))); // Determine results directory based on outputPath or use default let resultsDir; if (outputPath) { // Check if outputPath is a directory or file path const stats = await fs.stat(outputPath).catch(() => null); if (stats && stats.isDirectory()) { resultsDir = outputPath; } else if (outputPath.endsWith('.png') || outputPath.endsWith('.jpg') || outputPath.endsWith('.jpeg')) { resultsDir = path.dirname(outputPath); } else { // Assume it's a directory path if no file extension resultsDir = outputPath; } } else { resultsDir = path.join(projectPath, 'src', 'components', componentName, 'results'); } await ensureDirectory(resultsDir); // Ensure Vue dev server is running const port = 1932; console.log(chalk.blue('🚀 Checking Vue dev server...')); await this.ensureDevServerRunning(port); const screenshotResult = await this.takeSnapDOMScreenshot({ componentName, port, viewport, snapDOMOptions: { ...snapDOMOptions, scale: 3 }, resultsDir, outputPath: outputPath && (outputPath.endsWith('.png') || outputPath.endsWith('.jpg') || outputPath.endsWith('.jpeg')) ? outputPath : null, selector }); console.log(chalk.green('✅ snapDOM screenshot completed successfully!')); return { success: true, componentName, screenshot: screenshotResult, summary: { method: 'snapDOM', quality: 'high', outputPath: screenshotResult.path, features: screenshotResult.features } }; } catch (error) { // 处理不同类型的错误并提供具体解决方案 if (error instanceof PuppeteerLaunchError || error instanceof NetworkError || error instanceof PermissionError || error instanceof TimeoutError || error instanceof MemoryError) { console.error(chalk.red('❌ Screenshot failed:'), error.message); console.log(chalk.yellow('💡 Suggested solutions:')); error.solutions.forEach(solution => { console.log(chalk.yellow(` • ${solution}`)); }); return { success: false, error: error.message, errorType: error.name, solutions: error.solutions, componentName }; } else { console.error(chalk.red('❌ Screenshot failed:'), error.message); return { success: false, error: error.message, componentName }; } } } async ensureDevServerRunning(port) { // Skip server check and assume server is running console.log(chalk.green(`✅ Assuming Vue dev server is running on port ${port}`)); return true; } async takeSnapDOMScreenshot({ componentName, port, viewport, snapDOMOptions, resultsDir, outputPath, selector }) { console.log(chalk.gray(`📸 Starting simple screenshot...`)); // 使用页面池管理获取页面实例 const page = await puppeteerManager.getPage(); try { // Set viewport with 3x scale factor for high-resolution screenshots await page.setViewport({ width: viewport.width, height: viewport.height, deviceScaleFactor: 3 }); // Navigate to component let url = `http://localhost:${port}/component/${componentName}`; console.log(chalk.gray(`📍 Navigating to: ${url}`)); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 10000 }); // 简单等待 await page.waitForTimeout(500); // 确定选择器 let targetSelector = selector || '.screenshot-target'; console.log(chalk.gray(`🎯 Using selector: ${targetSelector}`)); // 等待元素 await page.waitForSelector(targetSelector, { timeout: 5000 }); // 简单截图 console.log(chalk.blue('📸 Taking screenshot...')); let screenshotPath; if (outputPath && (outputPath.endsWith('.png') || outputPath.endsWith('.jpg') || outputPath.endsWith('.jpeg'))) { screenshotPath = outputPath; } else { screenshotPath = path.join(resultsDir, 'actual.png'); } // 直接截图元素 const element = await page.$(targetSelector); if (!element) { throw new Error(`Component selector ${targetSelector} not found`); } const screenshotBuffer = await element.screenshot({ type: 'png', omitBackground: snapDOMOptions.backgroundColor === 'transparent' }); // 保存截图 console.log(chalk.gray(`💾 Saving screenshot to: ${screenshotPath}`)); await fs.writeFile(screenshotPath, screenshotBuffer); console.log(chalk.green(`✅ 3x scale Puppeteer screenshot saved: ${screenshotPath}`)); return { path: screenshotPath, url, selector: targetSelector, viewport: { ...viewport, actualWidth: viewport.width * 3, actualHeight: viewport.height * 3, scale: 3 }, snapDOMOptions, method: 'Puppeteer', quality: 'high', scale: 3, features: ['element-screenshot', 'transparent-background', 'high-quality', '3x-scale'] }; } finally { // 释放页面 await puppeteerManager.releasePage(page); } } } // Command line execution if (import.meta.url === `file://${process.argv[1]}`) { const componentName = process.argv[2]; if (!componentName) { console.error('Usage: node snapdom-screenshot.js <componentName>'); process.exit(1); } const tool = new SnapDOMScreenshotTool(); const projectPath = process.cwd(); const outputPath = path.join(projectPath, 'src', 'components', componentName, 'results', 'actual.png'); tool.execute({ componentName, projectPath, outputPath, viewport: { width: 1440, height: 800 }, snapDOMOptions: { backgroundColor: 'transparent', compress: true, deviceScaleFactor: 3, embedFonts: true } }).then(result => { console.log('Screenshot completed:', result.path); }).catch(error => { console.error('Screenshot failed:', error.message); process.exit(1); }); }