import { readFile, writeFile, parseRemoteType, generateScriptTag, injectScriptToHtml, removeInjectedScripts, log, logInfo, logSuccess, logError } from './utils.js'; import { FileFinder } from './file-finder.js'; import { CodeCleaner } from './code-cleaner.js'; import fs from 'fs'; import path from 'path'; // 检测项目类型 export function detectProjectType(startDir = process.cwd()) { const indicators = { 'vite': ['vite.config.js', 'vite.config.ts'], 'next-pages': ['next.config.js', 'pages/'], 'next-app': ['next.config.js', 'app/'], 'create-react-app': ['public/index.html', 'src/'], 'webpack': ['webpack.config.js'], 'html-static': ['index.html', '.html'] }; try { // 按优先级顺序检测 const priorityOrder = ['vite', 'next-app', 'next-pages', 'create-react-app', 'webpack', 'html-static']; for (const type of priorityOrder) { const files = indicators[type]; for (const file of files) { if (fs.existsSync(path.join(startDir, file))) { // 特殊处理 Next.js if (type.startsWith('next-')) { const hasPages = fs.existsSync(path.join(startDir, 'pages')); const hasApp = fs.existsSync(path.join(startDir, 'app')); if (hasApp && hasPages) { return 'next-hybrid'; // 两个目录都存在 } else if (hasApp) { return 'next-app'; } else if (hasPages) { return 'next-pages'; } } return type; } } } } catch (error) { logError(`项目类型检测失败: ${error.message}`); } return 'unknown'; } // 查找框架特定的入口文件 - 支持多次尝试机制 export function findFrameworkEntry(projectType, startDir = process.cwd()) { return FileFinder.findFrameworkEntry(projectType, startDir); } // 为 Vite 项目注入脚本插件 export function injectToViteConfig(filePath, remote, scriptTag) { try { // 检查文件是否存在 if (!fs.existsSync(path.join(process.cwd(), filePath))) { log(`Vite 配置文件 ${filePath} 不存在,跳过`, true); return false; } let content = readFile(filePath); // 先移除之前的注入(如果存在)- 支持新旧两种格式 // 使用字符串查找方式,确保能移除所有注入块(包括多个重复的情况) const startMarker = '// '; const endMarker = '// '; let startIndex = content.indexOf(startMarker); while (startIndex !== -1) { const endIndex = content.indexOf(endMarker, startIndex); if (endIndex !== -1) { // 找到结束标记,移除从开始标记到结束标记后的所有内容 // 注意:逗号在 })() 和注释结束之间,所以会被包含在移除范围内 let removeEnd = endIndex + endMarker.length; // 移除结束标记后的空白和换行 while (removeEnd < content.length && (content[removeEnd] === ' ' || content[removeEnd] === '\t' || content[removeEnd] === '\n')) { removeEnd++; } // 移除整个块:从开始标记(包括前面的换行和空白)到结束标记后 // 向前查找,移除开始标记前的换行和空白 let removeStart = startIndex; while (removeStart > 0 && (content[removeStart - 1] === ' ' || content[removeStart - 1] === '\t' || content[removeStart - 1] === '\n')) { removeStart--; if (content[removeStart] === '\n') { removeStart++; break; } } content = content.slice(0, removeStart) + content.slice(removeEnd); } else { // 没有找到结束标记,只移除开始标记 content = content.slice(0, startIndex) + content.slice(startIndex + startMarker.length); } // 继续查找下一个注入块 startIndex = content.indexOf(startMarker); } // 移除旧版格式(向后兼容) content = content.replace( /\/\/\s*dev-inject-plugin[\s\S]*?}\s*}\s*,?\s*/g, '' ); // 清理多余的逗号和空行 // 移除连续的逗号 content = content.replace(/,\s*,/g, ','); // 移除数组开头的逗号 content = content.replace(/\[\s*,/g, '['); // 移除数组元素之间多余的逗号 content = content.replace(/,\s*\)/g, ')'); content = content.replace(/,\s*\n\s*\]/g, '\n ]'); content = content.replace(/,\s*\n\s*\[/g, ' ['); // 清理多余的空行(保留最多一个空行) content = content.replace(/\n\s*\n\s*\n+/g, '\n\n'); // 生成插件代码,传入原始 remote URL 而不是 scriptTag const pluginCode = generateVitePlugin(remote, scriptTag); // 查找 plugins 数组,并在其中插入插件 const pluginsMatch = content.match(/(plugins:\s*\[)([\s\S]*?)(\])/); if (pluginsMatch) { const beforePlugins = pluginsMatch[1]; const pluginsContent = pluginsMatch[2].trim(); const afterPlugins = pluginsMatch[3]; // 插入插件到 plugins 数组开头 const newPlugins = beforePlugins + '\n ' + pluginCode + '\n ' + pluginsContent + '\n ' + afterPlugins; content = content.replace(pluginsMatch[0], newPlugins); } else { // 如果没有 plugins 数组,需要创建 log(`未找到 plugins 配置,Vite 配置可能不标准: ${filePath}`, true); return false; } writeFile(filePath, content); logSuccess(`已向 ${filePath} 注入 Vite 插件`); return true; } catch (error) { log(`Vite 插件注入失败: ${error.message}`, true); return false; } } // 更规范与健壮的版本 function generateVitePlugin(remote, { insertPosition = '' } = {}) { const safeRemote = JSON.stringify(remote); // 避免注入问题 const pluginName = 'dev-inject'; const scriptId = 'dev-inject-monitor'; const scriptInjection = ` `; return ` // { name: '${pluginName}', enforce: 'post', // 确保在 HTML 注入阶段最后执行 transformIndexHtml(html) { if (!html.includes('data-id="${scriptId}"')) { return html.replace(${JSON.stringify(insertPosition)}, \`${scriptInjection}\\n${insertPosition}\`); } return html; } }, // `; } // 为 Next.js 注入脚本到 _document export function injectToNextDocument(filePath, scriptTag, remote = null) { try { // 检查文件是否存在 if (!fs.existsSync(path.join(process.cwd(), filePath))) { log(`Next.js 文件 ${filePath} 不存在,跳过`, true); return false; } let content = readFile(filePath); // 检查是否是 _document.tsx/_document.js if (filePath.includes('_document')) { return injectToDocumentComponent(content, scriptTag, filePath, remote); } // 如果是 _app.tsx/_app.js if (filePath.includes('_app')) { return injectToAppComponent(content, scriptTag, filePath, remote); } log(`无法识别的 Next.js 文件类型: ${filePath}`, true); return false; } catch (error) { log(`Next.js 注入失败: ${error.message}`, true); return false; } } // 注入到 Next.js _document 组件 function injectToDocumentComponent(content, scriptTag, filePath, remote = null) { const isTypeScript = filePath.endsWith('.tsx'); // 先移除之前的注入 content = removeNextDocumentInjection(content); // 生成脚本加载代码(优先使用动态方式) let scriptInjection; if (remote) { // 使用动态脚本加载器(推荐方式,防止缓存) const scriptLoader = generateDynamicScriptLoader(remote); scriptInjection = `{/* DEV-INJECT-START */} {typeof window !== 'undefined' && ( )} {/* DEV-INJECT-END */}`; } else { // 向后兼容:使用旧的 scriptTag 方式 scriptInjection = `{/* DEV-INJECT-START */} {typeof window !== 'undefined' && ( ${scriptTag} )} {/* DEV-INJECT-END */}`; } // 查找
组件 const headRegex = /]*>([\s\S]*?)<\/Head>/; const headMatch = content.match(headRegex); if (headMatch) { // 在 组件内注入 - 使用 DEV-INJECT-START/END 标识 const beforeHead = headMatch[1]; const newHead = beforeHead + '\n ' + scriptInjection; // 修复多余的 < 符号问题 const headAttributes = headMatch[0].match(/]*)>/); const attributes = headAttributes ? headAttributes[1] : ''; content = content.replace(headRegex, `\n ${newHead}\n `); } else { // 如果没有 组件,需要添加 const importReactMatch = content.match(/import.*React.*from.*react/); if (importReactMatch) { const afterImport = content.indexOf(importReactMatch[0]) + importReactMatch[0].length; const headComponent = isTypeScript ? ` import Head from 'next/head';` : ` import Head from 'next/head';`; content = content.slice(0, afterImport) + headComponent + content.slice(afterImport); } // 在 Document 组件的 render 方法中添加 Head const renderMatch = content.match(/render\(\)\s*{[\s\S]*?return\s*\(/); if (renderMatch) { const insertPoint = content.indexOf(renderMatch[0]) + renderMatch[0].length; let headInsert; if (remote) { // 使用动态脚本加载器 const scriptLoader = generateDynamicScriptLoader(remote); headInsert = ` {/* DEV-INJECT-START */} {typeof window !== 'undefined' && ( )} {/* DEV-INJECT-END */} `; } else { // 向后兼容:使用旧的 scriptTag 方式 headInsert = ` {/* DEV-INJECT-START */} {typeof window !== 'undefined' && ${scriptTag.replace(/