#!/usr/bin/env node 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'), pendingLogicFileName: '.stitch-pending.json', pendingLogicMarker: 'STITCH_PENDING_LOGIC', }; 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 sanitizePageName(name) { return String(name) .replace(/[^a-z0-9-_]/gi, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .toLowerCase(); } function escapeForTemplateLiteral(value) { return String(value) .replace(/\\/g, '\\\\') .replace(/`/g, '\\`') .replace(/\$\{/g, '\\${'); } function extractTailwindConfig(html) { const startMatch = html.match(/tailwind\.config\s*=\s*\{/); if (!startMatch) return null; const startIndex = startMatch.index + startMatch[0].length - 1; let braceCount = 0; let endIndex = startIndex; for (let i = startIndex; i < html.length; i += 1) { if (html[i] === '{') braceCount += 1; else if (html[i] === '}') { braceCount -= 1; if (braceCount === 0) { endIndex = i + 1; break; } } } if (braceCount !== 0) return null; const configStr = html.substring(startIndex, endIndex); try { let cleanedStr = configStr .split('\n') .map((line) => { const commentIndex = line.indexOf('//'); if (commentIndex >= 0) { return line.substring(0, commentIndex).trimEnd(); } return line; }) .join('\n'); cleanedStr = cleanedStr.replace(/,(\s*[}\]])/g, '$1'); return (function () { return eval(`(${cleanedStr})`); }()); } catch (error) { console.error('[Stitch Converter] 解析 Tailwind 配置失败:', error.message); return null; } } function generateTailwindCSS(config) { if (!config || !config.theme) return ''; const theme = config.theme; const extend = theme.extend || {}; let css = ''; const processColors = (colors, prefix = '') => { let colorCSS = ''; for (const [name, value] of Object.entries(colors)) { if (typeof value === 'object' && !Array.isArray(value)) { colorCSS += processColors(value, `${prefix}${name}-`); } else { const cssName = `${prefix}${name}`.replace(/([A-Z])/g, '-$1').toLowerCase(); colorCSS += ` --color-${cssName}: ${value};\n`; } } return colorCSS; }; if (extend.colors) { css += '\n@theme {\n'; css += processColors(extend.colors); css += '}\n'; } if (extend.spacing) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.spacing)) { css += ` --spacing-${name}: ${value};\n`; } css += '}\n'; } if (extend.fontSize) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.fontSize)) { const size = Array.isArray(value) ? value[0] : value; css += ` --font-size-${name}: ${size};\n`; if (Array.isArray(value) && value[1]) { const lineHeight = typeof value[1] === 'object' ? value[1].lineHeight : value[1]; if (lineHeight) { css += ` --line-height-${name}: ${lineHeight};\n`; } } } css += '}\n'; } if (extend.fontFamily) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.fontFamily)) { const family = Array.isArray(value) ? value.join(', ') : value; css += ` --font-family-${name}: ${family};\n`; } css += '}\n'; } if (extend.borderRadius) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.borderRadius)) { const cssName = name === 'DEFAULT' ? 'default' : name; css += ` --radius-${cssName}: ${value};\n`; } css += '}\n'; } if (extend.boxShadow) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.boxShadow)) { const cssName = name === 'DEFAULT' ? 'default' : name; css += ` --shadow-${cssName}: ${value};\n`; } css += '}\n'; } if (extend.screens) { css += '\n/* 自定义断点 */\n'; for (const [name, value] of Object.entries(extend.screens)) { css += `/* @screen ${name}: ${value} */\n`; } } if (extend.zIndex) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.zIndex)) { css += ` --z-index-${name}: ${value};\n`; } css += '}\n'; } if (extend.opacity) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.opacity)) { css += ` --opacity-${name}: ${value};\n`; } css += '}\n'; } if (extend.keyframes) { css += '\n/* 动画关键帧 */\n'; for (const [name, frames] of Object.entries(extend.keyframes)) { css += `@keyframes ${name} {\n`; for (const [percent, styles] of Object.entries(frames)) { css += ` ${percent} {\n`; for (const [prop, val] of Object.entries(styles)) { const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase(); css += ` ${cssProp}: ${val};\n`; } css += ' }\n'; } css += '}\n\n'; } } if (extend.animation) { css += '\n/* 动画工具类 */\n'; css += '@layer utilities {\n'; for (const [name, value] of Object.entries(extend.animation)) { css += ` .animate-${name} {\n`; css += ` animation: ${value};\n`; css += ' }\n'; } css += '}\n\n'; } if (extend.transitionDuration) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.transitionDuration)) { css += ` --duration-${name}: ${value};\n`; } css += '}\n'; } if (extend.transitionTimingFunction) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.transitionTimingFunction)) { css += ` --ease-${name}: ${value};\n`; } css += '}\n'; } if (extend.backgroundImage) { css += '\n@theme {\n'; for (const [name, value] of Object.entries(extend.backgroundImage)) { css += ` --gradient-${name}: ${value};\n`; } css += '}\n'; } if (config.darkMode) { css += `\n/* Dark Mode: ${config.darkMode} */\n`; } return css; } function isTailwindConfigScript(attrs, content) { return /tailwind\.config/.test(content) || /id=["']tailwind-config["']/i.test(attrs); } function summarizeScript(script) { const source = script.content || script.src || ''; const normalized = source.toLowerCase(); if (/setinterval|settimeout/.test(normalized) && /time|date|clock/.test(normalized)) { return '更新时间或日期显示'; } if (/addEventListener\s*\(\s*['"]click['"]/.test(source) || /\.onclick\s*=/.test(source)) { return '点击交互处理'; } if (/addEventListener\s*\(\s*['"]scroll['"]/.test(source)) { return '滚动相关行为'; } if (/querySelector|getElementById|textContent|innerHTML/.test(source)) { return '页面内容或状态更新'; } if (script.src) { return `外部脚本:${script.src}`; } return '页面动态逻辑'; } function suggestMigration(script) { const source = script.content || script.src || ''; const normalized = source.toLowerCase(); if (/setinterval|settimeout/.test(normalized)) { return '迁移到 React.useEffect,并在需要时配合 useState 管理显示内容。'; } if (/addEventListener|onclick|onsubmit|onchange/.test(normalized)) { return '迁移到组件事件处理函数或 React.useEffect 的事件绑定。'; } if (/querySelector|getElementById|textContent|innerHTML/.test(source)) { return '改为通过组件状态、props 或 ref 更新界面,避免直接操作 DOM。'; } if (script.src) { return '确认该脚本的用途后,再决定是否以 npm 依赖、按需加载或替代实现接入。'; } return '建议拆解为组件状态、效果函数或独立 helper 后接入。'; } function createPendingScript(script, index) { return { id: script.id || `${script.location}-script-${index + 1}`, location: script.location, src: script.src || null, summary: summarizeScript(script), suggestedMigration: suggestMigration(script), code: script.content || '', }; } function extractHeadContent(html) { const headMatch = html.match(/
]*>([\s\S]*?)<\/head>/i); if (!headMatch) return { links: [], styles: [], pendingScripts: [] }; const headContent = headMatch[1]; const links = []; const styles = []; const pendingScripts = []; const scriptRegex = /