#!/usr/bin/env npx tsx /** * extract_inline_html.ts — Convert JSX/React mock files to self-contained HTML. * * Uses @babel/parser for proper JSX parsing instead of fragile regex. * Replaces the old extract_inline_html.py script. * * Usage: * npx tsx extract_inline_html.ts \ * --page src/MockPage.jsx:home.html:"Home Page" \ * --index-css src/index.css \ * --extra-css index.html \ * --outdir .stitch * * Flags: * --page Page spec as src_file:dst_filename:title (repeatable) * --tailwind-config Path to tailwind.config.js (auto-detected if omitted) * --no-tailwind Skip Tailwind CDN injection * --index-css Path to main CSS file * --css-files Additional CSS files (repeatable) * --extra-css Path to index.html to extract \n\n`; return head; } // --------------------------------------------------------------------------- // Main // --------------------------------------------------------------------------- async function main(): Promise { const opts = parseArgs(); validateOpts(opts); const startTime = Date.now(); const head = buildHead(opts); const stats: AllStats = { pages: [], totalImages: 0, durationMs: 0, warnings: [], }; fs.mkdirSync(opts.outdir, { recursive: true }); for (const spec of opts.pages) { const parts = spec.split(':'); const [src, dstName, title] = parts; const dst = path.join(opts.outdir, dstName); console.log(`\n${'='.repeat(60)}`); console.log(`Converting ${src} -> ${dstName}...`); console.log(`${'='.repeat(60)}`); const jsx = fs.readFileSync(src, 'utf-8'); let body = jsxToHtml(jsx); if (!body) { const msg = `Failed to parse JSX from ${src}`; console.error(` ${msg}`); stats.warnings.push(msg); continue; } // Apply exclude pattern (pre-validated during argument parsing) if (opts.excludePattern) { body = body.replace(opts.excludePattern, ''); } // Extract body class from outer wrapper div const outerMatch = body.match(/^]*>([\s\S]*)<\/div>$/); let fullHtml: string; if (outerMatch) { fullHtml = head.replace('{{title}}', title) + `\n${outerMatch[2].trim()}\n\n`; } else { fullHtml = head.replace('{{title}}', title) + `\n${body}\n\n`; } // Embed remote images with concurrency const cacheCountBefore = imgCache.size; fullHtml = await embedImages(fullHtml, opts.concurrency, opts.timeout); const imagesEmbedded = imgCache.size - cacheCountBefore; fs.writeFileSync(dst, fullHtml, 'utf-8'); const fileSize = fs.statSync(dst).size; console.log(`=> ${dst} (${fileSize.toLocaleString()} bytes)`); stats.pages.push({ src, dst, sizeBytes: fileSize, imagesEmbedded, }); } stats.totalImages = imgCache.size; stats.durationMs = Date.now() - startTime; console.log( `\nDONE: ${imgCache.size} unique images embedded in ${stats.durationMs}ms.`, ); if (opts.json) { console.log('\n--- JSON Stats ---'); console.log(JSON.stringify(stats, null, 2)); } } main().catch((err: Error) => { console.error('❌ Error:', err.message); if (err.stack) console.error(err.stack); process.exit(1); });