#!/usr/bin/env node /** * ===================================================== * Chrome 扩展导出转换器 * * 专门处理通过 Chrome 扩展本项目导出的 HTML 文件 * * 功能: * 1. 转换 index.html 为 React 组件 * 2. 智能处理字体:CDN 保留链接,本地文件复制 * 3. 复制静态资源(图片、字体) * 4. 保留完整的 style.css 样式 * ===================================================== */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { assertValidGeneratedTsx } from './utils/generatedTsxValidator.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const CONFIG = { projectRoot: path.resolve(__dirname, '..'), pagesDir: path.resolve(__dirname, '../src/prototypes') }; const JSX_ATTRIBUTE_REPLACEMENTS = [ ['class', 'className'], ['for', 'htmlFor'], ['tabindex', 'tabIndex'], ['readonly', 'readOnly'], ['maxlength', 'maxLength'], ['minlength', 'minLength'], ['colspan', 'colSpan'], ['rowspan', 'rowSpan'], ['viewbox', 'viewBox'], ['preserveaspectratio', 'preserveAspectRatio'], ['clip-path', 'clipPath'], ['fill-rule', 'fillRule'], ['clip-rule', 'clipRule'], ['stroke-width', 'strokeWidth'], ['stroke-dasharray', 'strokeDasharray'], ['stroke-dashoffset', 'strokeDashoffset'], ['stroke-linecap', 'strokeLinecap'], ['stroke-linejoin', 'strokeLinejoin'], ['stroke-miterlimit', 'strokeMiterlimit'], ['stroke-opacity', 'strokeOpacity'], ['fill-opacity', 'fillOpacity'], ['stop-color', 'stopColor'], ['stop-opacity', 'stopOpacity'], ['xlink:href', 'xlinkHref'], ['xmlns:xlink', 'xmlnsXlink'], ]; function log(message, type = 'info') { const prefix = { info: '✓', warn: '⚠', error: '✗', progress: '⏳' }[type] || 'ℹ'; console.log(`${prefix} ${message}`); } function ensureDir(dirPath) { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true }); } } function normalizeRelativeDir(value) { return String(value ?? '') .replace(/\\/g, '/') .split('/') .filter(Boolean) .join('/'); } function isSafeRelativeDir(value) { if (!value) return false; if (value.startsWith('/') || value.startsWith('~')) return false; const segments = value.split('/'); if (segments.length === 0) return false; return segments.every((segment) => { if (!segment || segment === '.' || segment === '..') return false; return !/[\\/]/.test(segment); }); } /** * 判断是否为 CDN 链接 */ function isCDNUrl(url) { return url.startsWith('http://') || url.startsWith('https://') || url.startsWith('//'); } /** * 递归复制目录 */ function copyDirectory(src, dest) { if (!fs.existsSync(src)) return 0; ensureDir(dest); const entries = fs.readdirSync(src, { withFileTypes: true }); let count = 0; for (const entry of entries) { const srcPath = path.join(src, entry.name); const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { count += copyDirectory(srcPath, destPath); } else { fs.copyFileSync(srcPath, destPath); count++; } } return count; } /** * 提取 head 内容(仅处理外部资源和字体) */ function extractHeadContent(html) { const headMatch = html.match(/
]*>([\s\S]*?)<\/head>/i); if (!headMatch) return { scripts: [], links: [] }; const headContent = headMatch[1]; const scripts = []; const links = []; // 提取 script 标签(排除 Tailwind CDN) const scriptRegex = /