UNPKG

v0-ui-reviewer

Version:

Next-gen UI/UX reviewer with multi-model AI support (OpenAI, Claude, v0), style extraction, and live preview sandbox

176 lines (175 loc) 7.41 kB
import puppeteer from 'puppeteer-extra'; import StealthPlugin from 'puppeteer-extra-plugin-stealth'; import { promises as fs } from 'fs'; import path from 'path'; import { logger } from './logger.js'; // Configure puppeteer-extra with stealth plugin puppeteer.use(StealthPlugin()); const DEFAULT_RETRY_STRATEGIES = [ { name: 'Standard', waitTime: 2000, waitUntil: 'networkidle0' }, { name: 'Quick DOM', waitTime: 5000, waitUntil: 'domcontentloaded' }, { name: 'Extended Wait', waitTime: 10000, waitUntil: ['domcontentloaded', 'networkidle2'] }, { name: 'Minimal', waitTime: 3000, waitUntil: 'load', browserArgs: ['--disable-images', '--disable-javascript'] } ]; export class EnhancedScreenshotCapture { verbose; constructor(verbose = false) { this.verbose = verbose; } log(message, level = 'info') { if (this.verbose || level === 'error' || level === 'warn') { logger[level](message); } } async captureWithRetry(url, options = {}) { const { retries = 3, onProgress } = options; let lastError = null; for (let attempt = 0; attempt <= retries; attempt++) { const strategy = DEFAULT_RETRY_STRATEGIES[Math.min(attempt, DEFAULT_RETRY_STRATEGIES.length - 1)]; try { this.log(`Attempt ${attempt + 1}/${retries + 1} using ${strategy.name} strategy`, 'debug'); onProgress?.('Screenshot Capture', (attempt / (retries + 1)) * 30, `Trying ${strategy.name} strategy...`); const result = await this.captureScreenshot(url, { ...options, strategy }); onProgress?.('Screenshot Capture', 100, 'Screenshot captured successfully'); return result; } catch (error) { lastError = error; this.log(`Attempt ${attempt + 1} failed: ${lastError.message}`, 'debug'); if (attempt < retries) { const waitTime = (attempt + 1) * 2000; // Progressive backoff this.log(`Waiting ${waitTime}ms before retry...`, 'debug'); await new Promise(resolve => setTimeout(resolve, waitTime)); } } } throw new Error(`Failed to capture screenshot after ${retries + 1} attempts. Last error: ${lastError?.message}`); } async captureScreenshot(url, options = {}) { const { fullPage = true, viewportWidth = 1920, viewportHeight = 1080, outputPath, mobile = false, timeout = 30000, onProgress, strategy = DEFAULT_RETRY_STRATEGIES[0] } = options; const browserArgs = [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-accelerated-2d-canvas', '--no-first-run', '--no-zygote', '--disable-gpu', '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process', '--disable-blink-features=AutomationControlled', ...(strategy.browserArgs || []) ]; this.log('Launching browser with stealth mode...', 'debug'); const browser = await puppeteer.launch({ headless: true, args: browserArgs, executablePath: puppeteer.executablePath(), timeout: 60000 }); try { const page = await browser.newPage(); // Enhanced stealth configuration await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); await page.setExtraHTTPHeaders({ 'Accept-Language': 'en-US,en;q=0.9' }); // Override navigator properties await page.evaluateOnNewDocument(() => { Object.defineProperty(navigator, 'webdriver', { get: () => false, }); // Add chrome object if (!window.chrome) { window.chrome = { runtime: {} }; } // Override permissions const originalQuery = window.navigator.permissions.query; window.navigator.permissions.query = (parameters) => (parameters.name === 'notifications' ? Promise.resolve({ state: 'denied' }) : originalQuery(parameters)); }); // Set viewport await page.setViewport({ width: mobile ? 375 : viewportWidth, height: mobile ? 667 : viewportHeight, deviceScaleFactor: mobile ? 2 : 1, isMobile: mobile, hasTouch: mobile }); this.log(`Navigating to ${url} with ${strategy.name} strategy...`, 'debug'); // Navigate with strategy-specific wait conditions await page.goto(url, { waitUntil: strategy.waitUntil, timeout }); // Additional wait time for dynamic content if (strategy.waitTime > 0) { this.log(`Waiting ${strategy.waitTime}ms for dynamic content...`, 'debug'); await new Promise(resolve => setTimeout(resolve, strategy.waitTime)); } // Scroll to trigger lazy loading await page.evaluate(() => { return new Promise((resolve) => { let totalHeight = 0; const distance = 100; const timer = setInterval(() => { const scrollHeight = document.body.scrollHeight; window.scrollBy(0, distance); totalHeight += distance; if (totalHeight >= scrollHeight) { clearInterval(timer); window.scrollTo(0, 0); // Scroll back to top setTimeout(resolve, 1000); // Wait for any final renders } }, 100); }); }); // Generate filename const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const domain = new URL(url).hostname.replace(/\./g, '-'); const filename = outputPath || `v0-review-${domain}-${timestamp}.png`; // Ensure output directory exists const outputDir = path.dirname(path.resolve(filename)); await fs.mkdir(outputDir, { recursive: true }); const screenshotPath = path.resolve(filename); this.log('Capturing screenshot...', 'debug'); // Capture screenshot with enhanced options await page.screenshot({ path: screenshotPath, fullPage, type: 'png', captureBeyondViewport: true, optimizeForSpeed: false }); this.log(`Screenshot saved to ${screenshotPath}`, 'debug'); return screenshotPath; } finally { await browser.close(); } } } export default EnhancedScreenshotCapture;