import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs'; import { join } from 'path'; /** * 查找项目中的 HTML 文件 */ export function lookupFiles(startDir = process.cwd()) { const htmlFiles = []; function searchDirectory(dir) { try { const files = readdirSync(dir); for (const file of files) { const fullPath = join(dir, file); const stat = statSync(fullPath); if (stat.isDirectory()) { // 跳过 node_modules 等目录 if (!['node_modules', '.git', 'dist', 'build'].includes(file)) { searchDirectory(fullPath); } } else if (file.endsWith('.html')) { htmlFiles.push(fullPath); } } } catch (error) { // 忽略无法访问的目录 } } searchDirectory(startDir); // 优先返回 index.html,否则返回第一个找到的 HTML 文件 const indexHtml = htmlFiles.find(file => file.endsWith('index.html')); return indexHtml ? [indexHtml] : htmlFiles; } /** * 读取文件内容 */ export function readFile(filePath) { try { return readFileSync(filePath, 'utf8'); } catch (error) { throw new Error(`无法读取文件 ${filePath}: ${error.message}`); } } /** * 写入文件内容 */ export function writeFile(filePath, content) { try { writeFileSync(filePath, content, 'utf8'); } catch (error) { throw new Error(`无法写入文件 ${filePath}: ${error.message}`); } } /** * 解析远程脚本类型 */ export function parseRemoteType(remote) { if (remote.startsWith('http://') || remote.startsWith('https://')) { return 'url'; } else if (remote.startsWith('/')) { return 'absolute-path'; } else { throw new Error( `不支持的远程路径格式: ${remote}。请使用完整 URL (http://...) 或绝对路径 (/path/to/script.js)` ); } } /** * 生成脚本标签 */ export function generateScriptTag(remote, type) { // 所有类型都添加时间戳参数避免缓存 const separator = remote.includes('?') ? '&' : '?'; const timestamp = Date.now(); if (type === 'url') { return ``; } else if (type === 'absolute-path') { return ``; } else { throw new Error(`未知的远程类型: ${type}`); } } /** * 从 HTML 内容中移除注入的脚本 - 同时清理前后的空白行 */ export function removeInjectedScripts(htmlContent, identifier = 'dev-inject') { // 使用标准的开始/结束标识移除注入的代码 // 匹配包括前后换行符和空白在内的完整块 // 使用非贪婪匹配确保只匹配一个注入块 const injectBlockRegex = /(\r?\n)?\s*[\s\S]*?\s*(\r?\n)?/g; let cleaned = htmlContent.replace(injectBlockRegex, (match, beforeNewline, afterNewline) => { // 如果匹配块前后都有换行符,只保留一个换行符 if (beforeNewline && afterNewline) { return '\n'; } // 如果只有前面的换行符,保留它 if (beforeNewline) { return beforeNewline; } // 如果只有后面的换行符,保留它 if (afterNewline) { return afterNewline; } // 如果都没有,返回空字符串 return ''; }); // 清理移除后可能产生的多余空白行 // 清理
标签后的多余空白行(保留一个换行) cleaned = cleaned.replace(/(]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2'); // 清理 前的多余空白行(保留一个换行) cleaned = cleaned.replace(/(\s*)\n\s*\n+(\s*<\/head>)/g, '$1\n$2'); // 清理连续的多个空白行(保留最多一个空行) cleaned = cleaned.replace(/\n\s*\n\s*\n+/g, '\n\n'); // 清理空行(只包含空白字符的行) cleaned = cleaned.replace(/^\s+$/gm, ''); return cleaned; } /** * 向 HTML 中注入脚本 - 使用动态时间戳防止缓存 */ export function injectScriptToHtml( htmlContent, scriptTag, identifier = 'dev-inject', remote = null ) { // 先清除之前的注入 htmlContent = removeInjectedScripts(htmlContent, identifier); // 生成脚本内容 - 优先使用动态时间戳方式 // 注意:脚本内容前后不包含额外的换行符,由插入逻辑统一处理 let scriptContent; if (remote) { // 使用动态脚本加载器(推荐方式,防止缓存) const safeRemote = JSON.stringify(remote); const scriptId = `${identifier}-monitor`; scriptContent = ` `; } else { // 向后兼容:从 scriptTag 中提取 URL 并添加时间戳 const scriptSrc = scriptTag.match(/src="([^"]*)"/)?.[1] || ''; if (scriptSrc) { // 提取基础 URL(移除可能已存在的时间戳参数) const baseUrl = scriptSrc.split('?')[0]; const separator = scriptSrc.includes('?') ? '&' : '?'; scriptContent = ` `; } else { // 如果无法提取 URL,使用旧的静态方式 scriptContent = ` ${scriptTag} `; } } // 查找 标签 const headEndIndex = htmlContent.lastIndexOf(''); if (headEndIndex !== -1) { // 在 之前注入 const beforeHead = htmlContent.substring(0, headEndIndex); const afterHead = htmlContent.substring(headEndIndex); // 检查 前是否有非空白内容 const beforeHeadTrimmed = beforeHead.trimEnd(); const needsNewlineBefore = beforeHeadTrimmed.length > 0 && !beforeHeadTrimmed.endsWith('\n'); // 构建注入内容,统一格式 let result; if (needsNewlineBefore) { // 如果前面有内容,添加换行符后插入脚本 result = `${beforeHeadTrimmed}\n${scriptContent}\n${afterHead}`; } else { // 如果前面是空白或换行,直接插入(脚本内容已有标识注释) // 确保脚本前后有适当的换行 const trimmedBefore = beforeHead.trimEnd(); if (trimmedBefore.length === 0) { // 如果前面完全是空白,直接插入 result = `${beforeHead}${scriptContent}\n${afterHead}`; } else { // 前面有内容但末尾是换行,直接插入 result = `${beforeHead}${scriptContent}\n${afterHead}`; } } // 清理可能的多余空白行(保留最多一个空行) // 清理 标签后的多余空白行 result = result.replace(/(]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2'); // 清理 前的多余空白行 result = result.replace(/(\s*)\n\s*\n+(\s*<\/head>)/g, '$1\n$2'); // 清理连续的多个空白行 result = result.replace(/\n\s*\n\s*\n+/g, '\n\n'); return result; } else { // 如果没有找到 ,查找 标签 const bodyStartIndex = htmlContent.indexOf('', bodyStartIndex) + 1; const beforeBody = htmlContent.substring(0, bodyEndIndex); const afterBody = htmlContent.substring(bodyEndIndex); return `${beforeBody} ${scriptContent} ${afterBody}`; } else { // 如果都没有找到,在文件末尾添加 return `${htmlContent} ${scriptContent}`; } } } /** * 日志输出函数 */ export function log(message, verbose = false) { if (verbose) { console.log(`[dev-inject] ${message}`); } } export function logError(message) { console.error(`❌ [dev-inject] ${message}`); } export function logSuccess(message) { console.log(`✅ [dev-inject] ${message}`); } export function logInfo(message) { console.log(`ℹ️ [dev-inject] ${message}`); }