/** * Website capture orchestrator. * * Two-pass capture approach: * Pass 1: Full page load (all JS) → catalog animations + snapshot canvases * Pass 2: Framework scripts blocked → extract stable HTML/CSS * * This ensures we get both: * - Rich animation metadata for Claude Code to recreate * - Stable, renderable HTML that won't crash in Puppeteer */ import { mkdirSync, writeFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { extractHtml } from "./htmlExtractor.js"; // captureScreenshots removed — full-page screenshot replaces per-section shots import { extractTokens } from "./tokenExtractor.js"; import { extractDesignStyles } from "./designStyleExtractor.js"; import { downloadAssets, downloadAndRewriteFonts } from "./assetDownloader.js"; import { extractFontMetadata } from "./fontMetadataExtractor.js"; import { normalizeErrorMessage } from "../utils/errorMessage.js"; // briefGenerator.ts, visual-style, capture-summary removed — DESIGN.md replaces them import { setupAnimationCapture, startCdpAnimationCapture, collectAnimationCatalog, } from "./animationCataloger.js"; import { saveLottieAnimations, renderLottiePreviews, captureVideoManifest, } from "./mediaCapture.js"; import type { DiscoveredLottie } from "./mediaCapture.js"; import { detectLibraries, extractVisibleText, captionImagesWithGemini, generateAssetDescriptions, } from "./contentExtractor.js"; import { loadEnvFile, generateProjectScaffold } from "./scaffolding.js"; import type { CaptureOptions, CaptureResult } from "./types.js"; export type { CaptureOptions, CaptureResult } from "./types.js"; // fallow-ignore-next-line complexity export async function captureWebsite( opts: CaptureOptions, onProgress?: (stage: string, detail?: string) => void, ): Promise { const { url, outputDir, viewportWidth = 1920, viewportHeight = 1080, timeout = 120000, settleTime = 3000, maxScreenshots: _maxScreenshots = 24, skipAssets = false, } = opts; const warnings: string[] = []; const progress = (stage: string, detail?: string) => { onProgress?.(stage, detail); }; // Load .env file from repo root if it exists (for GEMINI_API_KEY, etc.) loadEnvFile(outputDir); // Create output directories mkdirSync(join(outputDir, "extracted"), { recursive: true }); mkdirSync(join(outputDir, "screenshots"), { recursive: true }); mkdirSync(join(outputDir, "assets"), { recursive: true }); // Launch browser progress("browser", "Launching headless Chrome..."); const { ensureBrowser } = await import("../browser/manager.js"); const browser = await ensureBrowser(); const puppeteer = await import("puppeteer-core"); const chromeBrowser = await puppeteer.default.launch({ headless: true, executablePath: browser.executablePath, args: [ "--no-sandbox", "--disable-dev-shm-usage", "--enable-webgl", "--ignore-gpu-blocklist", "--use-gl=angle", "--use-angle=swiftshader", "--disable-blink-features=AutomationControlled", "--disable-background-timer-throttling", "--disable-renderer-backgrounding", `--window-size=${viewportWidth},${viewportHeight}`, ], }); let animationCatalog: CaptureResult["animationCatalog"]; try { // ═══════════════════════════════════════════════════════════════ // PASS 1: Full page load — all JS runs // Goal: Catalog animations + take screenshots (with JS rendering) // ═══════════════════════════════════════════════════════════════ progress("animations", "Cataloging animations (full JS)..."); const page1 = await chromeBrowser.newPage(); await page1.setViewport({ width: viewportWidth, height: viewportHeight }); await page1.setUserAgent( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", ); // Set up hooks BEFORE navigation await setupAnimationCapture(page1); const { cdp, animations: cdpAnims } = await startCdpAnimationCapture(page1); // Hook WebGL to capture shader source code (GLSL) // Captured shaders inform Claude Code about the site's visual effects // and enable reliable library detection (Three.js/PixiJS/Babylon.js uniforms survive bundling) await page1.evaluateOnNewDocument(` var origGetContext = HTMLCanvasElement.prototype.getContext; window.__capturedShaders = []; HTMLCanvasElement.prototype.getContext = function(type, attrs) { var ctx = origGetContext.call(this, type, attrs); if (ctx && (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl')) { if (ctx.shaderSource && !ctx.__hfHooked) { var origShaderSource = ctx.shaderSource.bind(ctx); ctx.shaderSource = function(shader, source) { try { var shaderType = ctx.getShaderParameter(shader, ctx.SHADER_TYPE); window.__capturedShaders.push({ type: shaderType === ctx.VERTEX_SHADER ? 'vertex' : 'fragment', source: source.slice(0, 5000) }); } catch(e) {} return origShaderSource(shader, source); }; ctx.__hfHooked = true; } } return ctx; }; `); // Intercept network responses to detect Lottie JSON files const discoveredLotties: DiscoveredLottie[] = []; // Layer 1 (passive video discovery): every direct-video URL the page fetches // over the whole session (load / scroll / carousel rotation), independent of // whether a