提交qiming-dev-inject
This commit is contained in:
48
qiming-dev-inject/lib/args.js
Normal file
48
qiming-dev-inject/lib/args.js
Normal file
@@ -0,0 +1,48 @@
|
||||
|
||||
export function parseArgs(args) {
|
||||
const result = {
|
||||
command: null,
|
||||
options: {},
|
||||
};
|
||||
|
||||
// 解析命令
|
||||
if (args[0] === 'install' || args[0] === 'uninstall') {
|
||||
result.command = args[0];
|
||||
args = args.slice(1);
|
||||
} else if (args.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 解析选项
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
if (arg.startsWith('--remote=')) {
|
||||
result.options.remote = arg.split('=')[1];
|
||||
} else if (arg === '--remote' && i + 1 < args.length) {
|
||||
result.options.remote = args[i + 1];
|
||||
i++; // 跳过下一个参数
|
||||
} else if (arg.startsWith('--file=')) {
|
||||
result.options.file = arg.split('=')[1];
|
||||
} else if (arg === '--file' && i + 1 < args.length) {
|
||||
result.options.file = args[i + 1];
|
||||
i++; // 跳过下一个参数
|
||||
} else if (arg === '--dry-run') {
|
||||
result.options.dryRun = true;
|
||||
} else if (arg === '--verbose') {
|
||||
result.options.verbose = true;
|
||||
} else if (arg === '--framework' || arg === '-f') {
|
||||
result.options.framework = true;
|
||||
} else if (!arg.startsWith('--')) {
|
||||
// 未知参数,可能是错误输入
|
||||
console.warn(`⚠️ 忽略未知参数: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 为 install 命令设置 remote 默认值
|
||||
if (result.command === 'install' && !result.options.remote) {
|
||||
result.options.remote = '/sdk/dev-monitor.js';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
112
qiming-dev-inject/lib/code-cleaner.js
Normal file
112
qiming-dev-inject/lib/code-cleaner.js
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 代码清理工具类 - 清理多余的空白字符和逗号
|
||||
*/
|
||||
export class CodeCleaner {
|
||||
/**
|
||||
* 清理多余的空白行
|
||||
* @param {string} content - 要清理的内容
|
||||
* @param {boolean} preserveDoubleNewline - 是否保留双换行(默认 true)
|
||||
* @returns {string} 清理后的内容
|
||||
*/
|
||||
static cleanBlankLines(content, preserveDoubleNewline = true) {
|
||||
if (preserveDoubleNewline) {
|
||||
// 保留单行空白,清理多余的连续空白行
|
||||
return content.replace(/\n\s*\n\s*\n+/g, '\n\n');
|
||||
} else {
|
||||
// 清理所有多余空白行,只保留单个换行
|
||||
return content.replace(/\n\s*\n+/g, '\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理行尾的空白字符
|
||||
* @param {string} content - 要清理的内容
|
||||
* @returns {string} 清理后的内容
|
||||
*/
|
||||
static cleanTrailingSpaces(content) {
|
||||
return content.replace(/[ \t]+$/gm, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理多余的逗号
|
||||
* @param {string} content - 要清理的内容
|
||||
* @returns {string} 清理后的内容
|
||||
*/
|
||||
static cleanCommas(content) {
|
||||
// 1. 清理单独的逗号行(整行都是逗号和空白)
|
||||
content = content.replace(/^\s*,\s*\n/gm, '');
|
||||
// 2. 清理行首的逗号和空白
|
||||
content = content.replace(/^\s*,\s+/gm, '');
|
||||
// 3. 清理连续的多个逗号
|
||||
content = content.replace(/,\s*,\s*/g, ',');
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理空的标签对
|
||||
* @param {string} content - 要清理的内容
|
||||
* @param {string} tagName - 标签名
|
||||
* @returns {string} 清理后的内容
|
||||
*/
|
||||
static cleanEmptyTags(content, tagName) {
|
||||
const regex = new RegExp(`<${tagName}[^>]*>\\s*</${tagName}>`, 'g');
|
||||
return content.replace(regex, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理标签中间的空白行
|
||||
* @param {string} content - 要清理的内容
|
||||
* @param {string} tagName - 标签名(可以是正则表达式)
|
||||
* @returns {string} 清理后的内容
|
||||
*/
|
||||
static cleanTagWhitespace(content, tagName) {
|
||||
// 清理如 <Head>\n\n\n</Head> -> <Head>\n</Head>
|
||||
content = content.replace(
|
||||
new RegExp(`(<${tagName}[^>]*>)\\s*\\n\\s*\\n(\\s*</${tagName}>)`, 'g'),
|
||||
'$1\n$2'
|
||||
);
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合清理:清理所有常见的代码格式问题
|
||||
* @param {string} content - 要清理的内容
|
||||
* @param {object} options - 清理选项
|
||||
* @returns {string} 清理后的内容
|
||||
*/
|
||||
static cleanAll(content, options = {}) {
|
||||
const {
|
||||
blankLines = true,
|
||||
trailingSpaces = true,
|
||||
commas = false,
|
||||
emptyTags = true,
|
||||
tagWhitespace = true
|
||||
} = options;
|
||||
|
||||
if (blankLines) {
|
||||
content = this.cleanBlankLines(content);
|
||||
}
|
||||
|
||||
if (trailingSpaces) {
|
||||
content = this.cleanTrailingSpaces(content);
|
||||
}
|
||||
|
||||
if (commas) {
|
||||
content = this.cleanCommas(content);
|
||||
}
|
||||
|
||||
if (emptyTags) {
|
||||
['head', 'Head'].forEach(tag => {
|
||||
content = this.cleanEmptyTags(content, tag);
|
||||
});
|
||||
}
|
||||
|
||||
if (tagWhitespace) {
|
||||
['Head', 'head'].forEach(tag => {
|
||||
content = this.cleanTagWhitespace(content, tag);
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
133
qiming-dev-inject/lib/file-finder.js
Normal file
133
qiming-dev-inject/lib/file-finder.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { lookupFiles } from './utils.js';
|
||||
|
||||
/**
|
||||
* 文件查找工具类 - 支持多种后缀名尝试机制
|
||||
*/
|
||||
export class FileFinder {
|
||||
/**
|
||||
* 查找具有多种后缀名的文件
|
||||
* @param {string} basePath - 基础路径(不包含后缀)
|
||||
* @param {string[]} extensions - 后缀名数组,按优先级排序
|
||||
* @param {string} startDir - 起始目录
|
||||
* @returns {string|null} 找到的文件路径或 null
|
||||
*/
|
||||
static findWithExtensions(basePath, extensions = ['.tsx', '.ts', '.jsx', '.js'], startDir = process.cwd()) {
|
||||
for (const ext of extensions) {
|
||||
const fullPath = path.join(startDir, basePath + ext);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
return basePath + ext;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找多个候选文件
|
||||
* @param {string[]} candidates - 候选文件路径数组
|
||||
* @param {string} startDir - 起始目录
|
||||
* @returns {string[]} 找到的文件路径数组
|
||||
*/
|
||||
static findMultiple(candidates, startDir = process.cwd()) {
|
||||
const foundFiles = [];
|
||||
for (const candidate of candidates) {
|
||||
if (candidate === '*.html') {
|
||||
// 特殊处理:查找所有 HTML 文件
|
||||
const htmlFiles = lookupFiles(startDir);
|
||||
foundFiles.push(...htmlFiles);
|
||||
} else if (fs.existsSync(path.join(startDir, candidate))) {
|
||||
foundFiles.push(candidate);
|
||||
}
|
||||
}
|
||||
return foundFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找框架特定的入口文件
|
||||
* @param {string} projectType - 项目类型
|
||||
* @param {string} startDir - 起始目录
|
||||
* @returns {string[]} 找到的文件路径数组
|
||||
*/
|
||||
static findFrameworkEntry(projectType, startDir = process.cwd()) {
|
||||
const foundFiles = [];
|
||||
|
||||
switch (projectType) {
|
||||
case 'vite':
|
||||
// 尝试查找 vite.config 文件
|
||||
const viteConfig = this.findWithExtensions('vite.config', ['.js', '.ts', '.mjs'], startDir);
|
||||
if (viteConfig) {
|
||||
foundFiles.push(viteConfig);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'next-pages':
|
||||
// 尝试查找 _document 文件
|
||||
const documentFile = this.findWithExtensions('pages/_document', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||
if (documentFile) {
|
||||
foundFiles.push(documentFile);
|
||||
}
|
||||
|
||||
// 尝试查找 _app 文件
|
||||
const appFile = this.findWithExtensions('pages/_app', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||
if (appFile) {
|
||||
foundFiles.push(appFile);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'next-app':
|
||||
// 尝试查找 layout 文件
|
||||
const layoutFile = this.findWithExtensions('app/layout', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||
if (layoutFile) {
|
||||
foundFiles.push(layoutFile);
|
||||
}
|
||||
|
||||
// 尝试查找 globals.css
|
||||
const globalsCss = 'app/globals.css';
|
||||
if (fs.existsSync(path.join(startDir, globalsCss))) {
|
||||
foundFiles.push(globalsCss);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'next-hybrid':
|
||||
// 尝试查找 app/layout 文件
|
||||
const appLayoutFile = this.findWithExtensions('app/layout', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||
if (appLayoutFile) {
|
||||
foundFiles.push(appLayoutFile);
|
||||
}
|
||||
|
||||
// 尝试查找 pages/_document 文件
|
||||
const pagesDocumentFile = this.findWithExtensions('pages/_document', ['.tsx', '.jsx', '.ts', '.js'], startDir);
|
||||
if (pagesDocumentFile) {
|
||||
foundFiles.push(pagesDocumentFile);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'create-react-app':
|
||||
// 尝试查找 HTML 文件
|
||||
const htmlFiles = ['public/index.html', 'index.html'];
|
||||
foundFiles.push(...this.findMultiple(htmlFiles, startDir));
|
||||
break;
|
||||
|
||||
case 'html-static':
|
||||
// 尝试查找 HTML 文件
|
||||
const staticHtmlFiles = ['index.html', '*.html'];
|
||||
foundFiles.push(...this.findMultiple(staticHtmlFiles, startDir));
|
||||
break;
|
||||
|
||||
default:
|
||||
// 默认查找 HTML 文件
|
||||
const defaultHtmlFiles = ['index.html', '*.html'];
|
||||
foundFiles.push(...this.findMultiple(defaultHtmlFiles, startDir));
|
||||
break;
|
||||
}
|
||||
|
||||
return foundFiles;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出便捷方法
|
||||
export function findFrameworkEntry(projectType, startDir = process.cwd()) {
|
||||
return FileFinder.findFrameworkEntry(projectType, startDir);
|
||||
}
|
||||
|
||||
850
qiming-dev-inject/lib/framework-inject.js
Normal file
850
qiming-dev-inject/lib/framework-inject.js
Normal file
@@ -0,0 +1,850 @@
|
||||
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 = '// <!-- DEV-INJECT-START -->';
|
||||
const endMarker = '// <!-- DEV-INJECT-END -->';
|
||||
|
||||
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 = '</head>' } = {}) {
|
||||
const safeRemote = JSON.stringify(remote); // 避免注入问题
|
||||
const pluginName = 'dev-inject';
|
||||
const scriptId = 'dev-inject-monitor';
|
||||
|
||||
const scriptInjection = `
|
||||
<script data-id="${scriptId}">
|
||||
(function() {
|
||||
const remote = ${safeRemote};
|
||||
const separator = remote.includes('?') ? '&' : '?';
|
||||
const script = document.createElement('script');
|
||||
script.src = remote + separator + 't=' + Date.now();
|
||||
script.dataset.id = '${scriptId}-script';
|
||||
script.defer = true;
|
||||
// 防止重复注入
|
||||
if (!document.querySelector('[data-id="${scriptId}-script"]')) {
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
`;
|
||||
|
||||
return `
|
||||
// <!-- DEV-INJECT-START -->
|
||||
{
|
||||
name: '${pluginName}',
|
||||
enforce: 'post', // 确保在 HTML 注入阶段最后执行
|
||||
transformIndexHtml(html) {
|
||||
if (!html.includes('data-id="${scriptId}"')) {
|
||||
return html.replace(${JSON.stringify(insertPosition)}, \`${scriptInjection}\\n${insertPosition}\`);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
},
|
||||
// <!-- DEV-INJECT-END -->
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
// 为 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' && (
|
||||
<script dangerouslySetInnerHTML={{
|
||||
__html: ${JSON.stringify(scriptLoader.trim())}
|
||||
}} />
|
||||
)}
|
||||
{/* DEV-INJECT-END */}`;
|
||||
} else {
|
||||
// 向后兼容:使用旧的 scriptTag 方式
|
||||
scriptInjection = `{/* DEV-INJECT-START */}
|
||||
{typeof window !== 'undefined' && (
|
||||
${scriptTag}
|
||||
)}
|
||||
{/* DEV-INJECT-END */}`;
|
||||
}
|
||||
|
||||
// 查找 <Head> 组件
|
||||
const headRegex = /<Head[^>]*>([\s\S]*?)<\/Head>/;
|
||||
const headMatch = content.match(headRegex);
|
||||
|
||||
if (headMatch) {
|
||||
// 在 <Head> 组件内注入 - 使用 DEV-INJECT-START/END 标识
|
||||
const beforeHead = headMatch[1];
|
||||
const newHead = beforeHead + '\n ' + scriptInjection;
|
||||
// 修复多余的 < 符号问题
|
||||
const headAttributes = headMatch[0].match(/<Head([^>]*)>/);
|
||||
const attributes = headAttributes ? headAttributes[1] : '';
|
||||
content = content.replace(headRegex, `<Head${attributes}>\n ${newHead}\n </Head>`);
|
||||
} else {
|
||||
// 如果没有 <Head> 组件,需要添加
|
||||
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 = `
|
||||
<Head>
|
||||
{/* DEV-INJECT-START */}
|
||||
{typeof window !== 'undefined' && (
|
||||
<script dangerouslySetInnerHTML={{
|
||||
__html: ${JSON.stringify(scriptLoader.trim())}
|
||||
}} />
|
||||
)}
|
||||
{/* DEV-INJECT-END */}
|
||||
</Head>`;
|
||||
} else {
|
||||
// 向后兼容:使用旧的 scriptTag 方式
|
||||
headInsert = `
|
||||
<Head>
|
||||
{/* DEV-INJECT-START */}
|
||||
{typeof window !== 'undefined' && ${scriptTag.replace(/<script[^>]*>|<\/script>/g, '')}}
|
||||
{/* DEV-INJECT-END */}
|
||||
</Head>`;
|
||||
}
|
||||
|
||||
content = content.slice(0, insertPoint) + headInsert + content.slice(insertPoint);
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(filePath, content);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 注入到 Next.js _app 组件
|
||||
function injectToAppComponent(content, scriptTag, filePath, remote = null) {
|
||||
// 移除之前的注入
|
||||
content = removeNextAppInjection(content);
|
||||
|
||||
// 在 _app 组件中注入,使用 useEffect
|
||||
const scriptLoader = generateScriptLoader(scriptTag, false, remote);
|
||||
const useEffectInsert = `
|
||||
{/* DEV-INJECT-START */}
|
||||
${scriptLoader}
|
||||
{/* DEV-INJECT-END */}`;
|
||||
|
||||
// 查找现有的 useEffect 或组件末尾
|
||||
const existingUseEffect = content.match(/useEffect\([^)]*\)\s*=>\s*{[\s\S]*?},\s*\[?\s*\]?\s*\);?/);
|
||||
|
||||
if (existingUseEffect) {
|
||||
// 在现有 useEffect 中添加
|
||||
content = content.replace(
|
||||
existingUseEffect[0],
|
||||
existingUseEffect[0].replace('}, []);', ' \n' + useEffectInsert.replace(/useEffect\([^)]*\)\s*=>\s*{\n/, '').replace(/}, \[\];/, '') + '\n }, [];')
|
||||
);
|
||||
} else {
|
||||
// 添加新的 useEffect
|
||||
const componentEndMatch = content.match(/export default.*|}\s*$/);
|
||||
if (componentEndMatch) {
|
||||
const insertPoint = content.lastIndexOf(componentEndMatch[0]);
|
||||
content = content.slice(0, insertPoint) + useEffectInsert + '\n\n' + content.slice(insertPoint);
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(filePath, content);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 为 Next.js App Router 注入
|
||||
export function injectToNextAppLayout(filePath, scriptTag, remote = null) {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(path.join(process.cwd(), filePath))) {
|
||||
log(`Next.js App Router 文件 ${filePath} 不存在,跳过`, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
let content = readFile(filePath);
|
||||
|
||||
// 移除之前的注入
|
||||
content = removeNextAppLayoutInjection(content);
|
||||
|
||||
// 在 layout.tsx/js 中注入到 <head> 内
|
||||
const scriptLoader = generateScriptLoader(scriptTag, true, remote);
|
||||
|
||||
// 查找 <head> 标签
|
||||
const headRegex = /<head[^>]*>([\s\S]*?)<\/head>/i;
|
||||
const headMatch = content.match(headRegex);
|
||||
|
||||
if (headMatch) {
|
||||
// 在 <head> 内注入
|
||||
const beforeHead = headMatch[1];
|
||||
const newHead = beforeHead + '\n ' + scriptLoader;
|
||||
content = content.replace(headRegex, `<head${headMatch[0].slice(5, -6)}${newHead}</head>`);
|
||||
} else {
|
||||
// 如果没有 <head> 标签,在 <html> 标签后添加
|
||||
const htmlRegex = /<html[^>]*>/;
|
||||
const htmlMatch = content.match(htmlRegex);
|
||||
if (htmlMatch) {
|
||||
const insertPoint = content.indexOf(htmlMatch[0]) + htmlMatch[0].length;
|
||||
const headTag = '\n <head>\n ' + scriptLoader + '\n </head>';
|
||||
content = content.slice(0, insertPoint) + headTag + content.slice(insertPoint);
|
||||
}
|
||||
}
|
||||
|
||||
writeFile(filePath, content);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
log(`Next.js App Router 注入失败: ${error.message}`, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成动态脚本加载器 - 使用运行时时间戳防止缓存
|
||||
function generateDynamicScriptLoader(remote, scriptId = 'dev-inject-monitor') {
|
||||
const safeRemote = JSON.stringify(remote); // 避免注入问题
|
||||
return `
|
||||
(function() {
|
||||
const remote = ${safeRemote};
|
||||
const separator = remote.includes('?') ? '&' : '?';
|
||||
const script = document.createElement('script');
|
||||
script.src = remote + separator + 't=' + Date.now();
|
||||
script.dataset.id = '${scriptId}-script';
|
||||
script.defer = true;
|
||||
// 防止重复注入
|
||||
if (!document.querySelector('[data-id="${scriptId}-script"]')) {
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
})();
|
||||
`;
|
||||
}
|
||||
|
||||
// 生成脚本加载器 - 使用 DEV-INJECT 标识和动态时间戳
|
||||
function generateScriptLoader(scriptTag, isHead = false, remote = null) {
|
||||
// 如果提供了 remote URL,使用动态脚本加载器(推荐方式)
|
||||
if (remote) {
|
||||
const scriptLoader = generateDynamicScriptLoader(remote);
|
||||
|
||||
if (isHead) {
|
||||
// 对于 App Router layout,注入到 <head> 内
|
||||
return `{/* DEV-INJECT-START */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<script dangerouslySetInnerHTML={{
|
||||
__html: ${JSON.stringify(scriptLoader.trim())}
|
||||
}} />
|
||||
)}
|
||||
{/* DEV-INJECT-END */}`;
|
||||
}
|
||||
// useEffect 方式(用于 _app 组件)
|
||||
return `useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
${scriptLoader.trim()}
|
||||
}
|
||||
}, []);`;
|
||||
}
|
||||
|
||||
// 向后兼容:如果没有提供 remote,使用旧的 scriptTag 方式
|
||||
if (isHead) {
|
||||
// 对于 App Router layout,注入到 <head> 内
|
||||
return `{/* DEV-INJECT-START */}
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
${scriptTag}
|
||||
)}
|
||||
{/* DEV-INJECT-END */}`;
|
||||
}
|
||||
// 旧版 useEffect 方式(保留向后兼容)
|
||||
return `useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
${scriptTag.replace(/<script[^>]*>|<\/script>/g, '').trim()}
|
||||
}
|
||||
}, []);`;
|
||||
}
|
||||
|
||||
// 通用的移除注入内容函数
|
||||
function removeInjectionContent(content, startMarker = '/* DEV-INJECT-START */', endMarker = '/* DEV-INJECT-END */') {
|
||||
let startIndex = content.indexOf(startMarker);
|
||||
while (startIndex !== -1) {
|
||||
const endIndex = content.indexOf(endMarker, startIndex);
|
||||
|
||||
if (endIndex !== -1) {
|
||||
// 找到这两行前后的换行符
|
||||
const startLineIndex = content.lastIndexOf('\n', startIndex - 1);
|
||||
const endLineEnd = content.indexOf('\n', endIndex + endMarker.length);
|
||||
|
||||
if (startLineIndex !== -1 && endLineEnd !== -1) {
|
||||
// 移除整块内容
|
||||
content = content.slice(0, startLineIndex + 1) + content.slice(endLineEnd + 1);
|
||||
} else if (startIndex >= 0 && endIndex >= startIndex) {
|
||||
// 简单的字符串切片
|
||||
const endPos = endIndex + endMarker.length;
|
||||
const endLine = content.indexOf('\n', endPos);
|
||||
if (endLine !== -1) {
|
||||
content = content.slice(0, startIndex) + content.slice(endLine + 1);
|
||||
} else {
|
||||
content = content.slice(0, startIndex) + content.slice(endIndex + endMarker.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 继续查找下一个
|
||||
startIndex = content.indexOf(startMarker);
|
||||
}
|
||||
|
||||
// 清理多余的空白行
|
||||
content = content.replace(/\n\s*\n\s*\n+/g, '\n\n');
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// 移除 Next.js Document 注入
|
||||
function removeNextDocumentInjection(content) {
|
||||
return removeInjectionContent(content);
|
||||
}
|
||||
|
||||
// 移除 Next.js App 注入
|
||||
function removeNextAppInjection(content) {
|
||||
// 移除 DEV-INJECT 相关的 useEffect(只保留新版标准标识)
|
||||
const patterns = [
|
||||
/\{\/\*\s*DEV-INJECT-START\s*\/\*\/[\s\S]*?\{\/\*\s*DEV-INJECT-END\s*\/\*\/\}/g,
|
||||
/useEffect\(\(\) => \{\s*if \(typeof window !== 'undefined'\) \{[\s\S]*?\}\s*\}, \[\]\);?/g
|
||||
];
|
||||
|
||||
patterns.forEach(pattern => {
|
||||
content = content.replace(pattern, '');
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// 移除 Next.js App Layout 注入
|
||||
function removeNextAppLayoutInjection(content) {
|
||||
// 使用字符串匹配移除 DEV-INJECT-START/END 之间的内容
|
||||
const startMarker = '/* DEV-INJECT-START */';
|
||||
const endMarker = '/* DEV-INJECT-END */';
|
||||
|
||||
let startIndex = content.indexOf(startMarker);
|
||||
while (startIndex !== -1) {
|
||||
const endIndex = content.indexOf(endMarker, startIndex);
|
||||
|
||||
if (endIndex !== -1) {
|
||||
// 找到这两行前后的换行符
|
||||
const startLineIndex = content.lastIndexOf('\n', startIndex - 1);
|
||||
const endLineEnd = content.indexOf('\n', endIndex + endMarker.length);
|
||||
|
||||
if (startLineIndex !== -1 && endLineEnd !== -1) {
|
||||
// 移除整块内容
|
||||
content = content.slice(0, startLineIndex + 1) + content.slice(endLineEnd + 1);
|
||||
} else {
|
||||
const endPos = endIndex + endMarker.length;
|
||||
const endLine = content.indexOf('\n', endPos);
|
||||
if (endLine !== -1) {
|
||||
content = content.slice(0, startIndex) + content.slice(endLine + 1);
|
||||
} else {
|
||||
content = content.slice(0, startIndex) + content.slice(endIndex + endMarker.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startIndex = content.indexOf(startMarker);
|
||||
}
|
||||
|
||||
// 使用 CodeCleaner 清理代码格式
|
||||
content = CodeCleaner.cleanAll(content, {
|
||||
blankLines: true,
|
||||
trailingSpaces: true,
|
||||
emptyTags: true,
|
||||
tagWhitespace: true
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// 移除 Vite 插件注入
|
||||
function removeViteInjection(content) {
|
||||
// 移除新版标准标识的注入(优先),包括后面的逗号
|
||||
content = content.replace(
|
||||
/\/\/ <!-- DEV-INJECT-START -->[\s\S]*?\/\/ <!-- DEV-INJECT-END -->\s*,?\s*/g,
|
||||
''
|
||||
);
|
||||
|
||||
// 移除旧版格式的注入(向后兼容)
|
||||
content = content.replace(
|
||||
/\/\/ dev-inject-plugin[\s\S]*?}\s*}\s*,?\s*/g,
|
||||
''
|
||||
);
|
||||
|
||||
// 使用 CodeCleaner 清理代码格式
|
||||
content = CodeCleaner.cleanAll(content, {
|
||||
blankLines: true,
|
||||
commas: true
|
||||
});
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// 智能卸载 - 智能移除不同框架的注入
|
||||
export function smartUninstall(options = {}) {
|
||||
const { file, dryRun = false, verbose = false } = options;
|
||||
|
||||
logInfo('开始移除注入的脚本...');
|
||||
|
||||
// 检测项目类型
|
||||
const projectType = detectProjectType();
|
||||
// 只在 verbose 模式下输出检测到的项目类型(避免重复)
|
||||
log(`检测到项目类型: ${projectType}`, verbose);
|
||||
|
||||
// 查找入口文件
|
||||
const entryFiles = file ? [file] : findFrameworkEntry(projectType);
|
||||
|
||||
if (entryFiles.length === 0) {
|
||||
logInfo('未找到需要处理的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`找到 ${entryFiles.length} 个文件:`, verbose);
|
||||
entryFiles.forEach(f => log(` - ${f}`, verbose));
|
||||
|
||||
let successCount = 0;
|
||||
|
||||
for (const entryFile of entryFiles) {
|
||||
try {
|
||||
if (projectType === 'next-pages' || projectType === 'next-hybrid') {
|
||||
if (entryFile.includes('_document')) {
|
||||
successCount += uninstallFromNextDocument(entryFile, dryRun, verbose);
|
||||
} else if (entryFile.includes('_app')) {
|
||||
successCount += uninstallFromNextApp(entryFile, dryRun, verbose);
|
||||
}
|
||||
} else if (projectType === 'next-app') {
|
||||
successCount += uninstallFromNextAppLayout(entryFile, dryRun, verbose);
|
||||
} else if (projectType === 'vite') {
|
||||
successCount += uninstallFromViteConfig(entryFile, dryRun, verbose);
|
||||
} else if (projectType === 'html-static' || projectType === 'create-react-app') {
|
||||
successCount += uninstallFromHTML(entryFile, dryRun, verbose);
|
||||
}
|
||||
} catch (error) {
|
||||
logError(`处理文件 ${entryFile} 失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
logSuccess(`成功清理 ${successCount} 个文件`);
|
||||
} else {
|
||||
logInfo('没有找到需要清理的注入脚本');
|
||||
}
|
||||
}
|
||||
|
||||
// 从 HTML 文件卸载
|
||||
function uninstallFromHTML(filePath, dryRun, verbose) {
|
||||
const content = readFile(filePath);
|
||||
const originalContent = content;
|
||||
const newContent = removeInjectedScripts(content);
|
||||
|
||||
if (newContent === originalContent) {
|
||||
log(`文件 ${filePath} 无需清理`, verbose);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
writeFile(filePath, newContent);
|
||||
logSuccess(`已从 ${filePath} 移除注入的脚本`);
|
||||
} else {
|
||||
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的脚本`);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 从 Vite 配置卸载
|
||||
function uninstallFromViteConfig(filePath, dryRun, verbose) {
|
||||
const content = readFile(filePath);
|
||||
const originalContent = content;
|
||||
const newContent = removeViteInjection(content);
|
||||
|
||||
if (newContent === originalContent) {
|
||||
log(`文件 ${filePath} 无需清理`, verbose);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
writeFile(filePath, newContent);
|
||||
logSuccess(`已从 ${filePath} 移除注入的 Vite 插件`);
|
||||
} else {
|
||||
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的插件`);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 从 Next.js Document 卸载
|
||||
function uninstallFromNextDocument(filePath, dryRun, verbose) {
|
||||
const content = readFile(filePath);
|
||||
const originalContent = content;
|
||||
const newContent = removeNextDocumentInjection(content);
|
||||
|
||||
if (newContent === originalContent) {
|
||||
log(`文件 ${filePath} 无需清理`, verbose);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
writeFile(filePath, newContent);
|
||||
logSuccess(`已从 ${filePath} 移除注入的脚本`);
|
||||
} else {
|
||||
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的脚本`);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 从 Next.js App 卸载
|
||||
function uninstallFromNextApp(filePath, dryRun, verbose) {
|
||||
const content = readFile(filePath);
|
||||
const originalContent = content;
|
||||
const newContent = removeNextAppInjection(content);
|
||||
|
||||
if (newContent === originalContent) {
|
||||
log(`文件 ${filePath} 无需清理`, verbose);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
writeFile(filePath, newContent);
|
||||
logSuccess(`已从 ${filePath} 移除注入的脚本`);
|
||||
} else {
|
||||
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的脚本`);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 从 Next.js App Layout 卸载
|
||||
function uninstallFromNextAppLayout(filePath, dryRun, verbose) {
|
||||
const content = readFile(filePath);
|
||||
const originalContent = content;
|
||||
const newContent = removeNextAppLayoutInjection(content);
|
||||
|
||||
if (newContent === originalContent) {
|
||||
log(`文件 ${filePath} 无需清理`, verbose);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
writeFile(filePath, newContent);
|
||||
logSuccess(`已从 ${filePath} 移除注入的脚本`);
|
||||
} else {
|
||||
logInfo(`[DRY RUN] 将从 ${filePath} 移除注入的脚本`);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 智能注入 - 根据项目类型选择最佳注入方式
|
||||
export function smartInject(options = {}) {
|
||||
const { remote, file, dryRun = false, verbose = false } = options;
|
||||
|
||||
// 检测项目类型
|
||||
const projectType = detectProjectType();
|
||||
// 只在 verbose 模式下输出检测到的项目类型(避免重复)
|
||||
log(`检测到项目类型: ${projectType}`, verbose);
|
||||
|
||||
// 生成脚本标签
|
||||
const remoteType = parseRemoteType(remote);
|
||||
const scriptTag = generateScriptTag(remote, remoteType);
|
||||
|
||||
log(`脚本类型: ${remoteType}`, verbose);
|
||||
log(`脚本标签: ${scriptTag}`, verbose);
|
||||
|
||||
// 查找入口文件
|
||||
const entryFiles = file ? [file] : findFrameworkEntry(projectType);
|
||||
|
||||
if (entryFiles.length === 0) {
|
||||
logError('未找到需要处理的文件');
|
||||
return false;
|
||||
}
|
||||
|
||||
log(`找到 ${entryFiles.length} 个文件:`, verbose);
|
||||
entryFiles.forEach(f => log(` - ${f}`, verbose));
|
||||
|
||||
let successCount = 0;
|
||||
const errors = [];
|
||||
|
||||
for (const entryFile of entryFiles) {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(path.join(process.cwd(), entryFile))) {
|
||||
log(`文件 ${entryFile} 不存在,跳过`, verbose);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理 Next.js Pages Router
|
||||
if (projectType === 'next-pages' || projectType === 'next-hybrid') {
|
||||
if (entryFile.includes('_document') || entryFile.includes('_app')) {
|
||||
if (injectToNextDocument(entryFile, scriptTag, remote)) {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 处理 Next.js App Router
|
||||
else if (projectType === 'next-app') {
|
||||
if (injectToNextAppLayout(entryFile, scriptTag, remote)) {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
// 处理 Vite 项目
|
||||
else if (projectType === 'vite') {
|
||||
if (injectToViteConfig(entryFile, remote, scriptTag)) {
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
// 处理 HTML 和 create-react-app
|
||||
else if (projectType === 'html-static' || projectType === 'create-react-app') {
|
||||
let htmlContent = readFile(entryFile);
|
||||
const originalContent = htmlContent;
|
||||
// 传入 remote URL 以使用动态时间戳防止缓存
|
||||
const newContent = injectScriptToHtml(htmlContent, scriptTag, 'dev-inject', remote);
|
||||
|
||||
if (newContent !== originalContent) {
|
||||
if (!dryRun) {
|
||||
writeFile(entryFile, newContent);
|
||||
logSuccess(`已注入脚本到 ${entryFile}`);
|
||||
} else {
|
||||
logInfo(`[DRY RUN] 将注入脚本到 ${entryFile}`);
|
||||
}
|
||||
successCount++;
|
||||
} else {
|
||||
log(`文件 ${entryFile} 已包含注入内容`, verbose);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 收集错误,但不立即报错
|
||||
errors.push(`处理文件 ${entryFile} 失败: ${error.message}`);
|
||||
log(`处理文件 ${entryFile} 失败: ${error.message}`, verbose);
|
||||
}
|
||||
}
|
||||
|
||||
// 只有在所有尝试都失败后才报错
|
||||
if (successCount > 0) {
|
||||
logSuccess(`成功处理 ${successCount} 个文件`);
|
||||
|
||||
// 只在 verbose 模式下输出详细信息
|
||||
log(`项目类型: ${projectType}`, verbose);
|
||||
log(`脚本地址: ${remote}`, verbose);
|
||||
|
||||
// 统一输出重启提示(不重复)
|
||||
if (projectType.startsWith('next-')) {
|
||||
logInfo('💡 Next.js 项目: 脚本已注入到组件中,需要重启开发服务器');
|
||||
} else if (projectType === 'vite') {
|
||||
logInfo('💡 Vite 项目: 脚本已注入到配置文件,需要重启开发服务器');
|
||||
}
|
||||
|
||||
// 如果有部分错误,显示警告
|
||||
if (errors.length > 0) {
|
||||
logInfo(`⚠️ 部分文件处理失败: ${errors.length} 个错误`);
|
||||
errors.forEach(error => log(` - ${error}`, verbose));
|
||||
}
|
||||
} else {
|
||||
// 所有尝试都失败了
|
||||
logError('没有成功处理任何文件');
|
||||
if (errors.length > 0) {
|
||||
logError('错误详情:');
|
||||
errors.forEach(error => logError(` - ${error}`));
|
||||
}
|
||||
}
|
||||
|
||||
return successCount > 0;
|
||||
}
|
||||
115
qiming-dev-inject/lib/help.js
Normal file
115
qiming-dev-inject/lib/help.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
export function showHelp() {
|
||||
console.log(`
|
||||
@xagi/dev-inject - 开发环境脚本注入工具
|
||||
|
||||
用法:
|
||||
npx @xagi/dev-inject <命令> [选项]
|
||||
|
||||
或使用 pnpm:
|
||||
pnpm dlx @xagi/dev-inject <命令> [选项]
|
||||
|
||||
命令:
|
||||
install 注入脚本到 HTML 文件
|
||||
uninstall 从 HTML 文件中移除注入的脚本
|
||||
|
||||
选项:
|
||||
--remote <url|path> 脚本地址(可选,默认值: /sdk/dev-monitor.js)
|
||||
- 远程URL: http://localhost:9000/monitor.js
|
||||
- 绝对路径: /scripts/monitor.js
|
||||
|
||||
--file <path> 指定要处理的 HTML 文件(可选)
|
||||
- 如果不指定,会自动查找项目中的 HTML 文件
|
||||
|
||||
--dry-run 预览模式,显示将要执行的操作但不实际修改文件
|
||||
|
||||
--verbose 显示详细输出信息
|
||||
|
||||
--framework, -f 使用框架感知注入模式
|
||||
- 自动检测 Vite、Next.js 等框架
|
||||
- 使用最佳注入方式,无需手动修改 HTML
|
||||
|
||||
--help, -h 显示帮助信息
|
||||
--version, -v 显示版本信息
|
||||
|
||||
示例:
|
||||
# 静默安装(推荐,自动确认,无需交互)
|
||||
npx -y @xagi/dev-inject install --framework
|
||||
|
||||
# 使用默认脚本地址 (/sdk/dev-monitor.js)
|
||||
npx @xagi/dev-inject install
|
||||
|
||||
# 使用默认地址 + 框架感知注入
|
||||
npx @xagi/dev-inject install --framework
|
||||
|
||||
# 传统 HTML 注入
|
||||
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js
|
||||
|
||||
# 框架感知注入(推荐)
|
||||
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||
|
||||
# Vite 项目自动注入
|
||||
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js -f --verbose
|
||||
|
||||
# Next.js App Router 注入
|
||||
npx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||
|
||||
# Next.js Pages Router 注入
|
||||
npx @xagi/dev-inject install --remote=/scripts/dev-monitor.js -f
|
||||
|
||||
# 指定特定 HTML 文件(传统模式)
|
||||
npx @xagi/dev-inject install --remote=/scripts/monitor.js --file=./public/index.html
|
||||
|
||||
# 使用 pnpm dlx
|
||||
pnpm dlx @xagi/dev-inject install --remote=http://localhost:9000/dev-monitor.js --framework
|
||||
|
||||
# 预览将要执行的操作
|
||||
npx @xagi/dev-inject install --remote=/scripts/monitor.js --framework --dry-run
|
||||
|
||||
# 移除框架注入
|
||||
npx @xagi/dev-inject uninstall --framework
|
||||
|
||||
# 移除传统注入
|
||||
npx @xagi/dev-inject uninstall
|
||||
|
||||
支持的脚本格式:
|
||||
- 远程 HTTP/HTTPS URL
|
||||
- 以 / 开头的绝对路径(相对于网站根目录)
|
||||
|
||||
框架支持:
|
||||
🟢 Vite: 自动注入 Vite 插件到 vite.config.js
|
||||
🟢 Next.js App Router: 注入到 app/layout.tsx
|
||||
🟢 Next.js Pages Router: 注入到 pages/_document.tsx
|
||||
🟢 Create React App: 注入到 public/index.html
|
||||
🟢 传统 HTML: 直接修改 HTML 文件
|
||||
|
||||
框架注入优势:
|
||||
✅ 自动检测项目类型
|
||||
✅ 使用最佳注入方式
|
||||
✅ 仅在开发环境生效
|
||||
✅ 零侵入性修改
|
||||
✅ 支持热重载
|
||||
|
||||
注意:
|
||||
- 使用 --framework 会自动选择最佳注入方式
|
||||
- 框架注入后需要重启开发服务器
|
||||
- 传统模式脚本会被注入到 HTML 文件的 </head> 标签之前
|
||||
- 重复执行 install 会自动替换之前的注入
|
||||
- 使用 uninstall 可以完全移除所有注入的脚本
|
||||
`);
|
||||
}
|
||||
|
||||
export function showVersion() {
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const packagePath = join(__dirname, '../package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||
console.log(`@xagi/dev-inject v${packageJson.version}`);
|
||||
} catch (error) {
|
||||
console.log('@xagi/dev-inject v1.0.0');
|
||||
}
|
||||
}
|
||||
206
qiming-dev-inject/lib/inject.js
Normal file
206
qiming-dev-inject/lib/inject.js
Normal file
@@ -0,0 +1,206 @@
|
||||
import {
|
||||
lookupFiles,
|
||||
readFile,
|
||||
writeFile,
|
||||
parseRemoteType,
|
||||
generateScriptTag,
|
||||
injectScriptToHtml,
|
||||
removeInjectedScripts,
|
||||
log,
|
||||
logInfo,
|
||||
logSuccess,
|
||||
logError,
|
||||
} from './utils.js';
|
||||
import {
|
||||
detectProjectType,
|
||||
smartInject as smartInjectFramework,
|
||||
} from './framework-inject.js';
|
||||
import { readFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
/**
|
||||
* 获取当前包版本号
|
||||
*/
|
||||
function getVersion() {
|
||||
try {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const packagePath = join(__dirname, '..', 'package.json');
|
||||
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||
return packageJson.version;
|
||||
} catch (error) {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装脚本 - 支持框架感知
|
||||
*/
|
||||
export async function installScript(options = {}) {
|
||||
const {
|
||||
remote,
|
||||
file,
|
||||
dryRun = false,
|
||||
verbose = false,
|
||||
framework = false,
|
||||
} = options;
|
||||
|
||||
const version = getVersion();
|
||||
logInfo(`开始执行脚本注入... (v${version})`);
|
||||
|
||||
// 检测是否使用框架注入模式
|
||||
if (framework) {
|
||||
logInfo('使用框架感知注入模式...');
|
||||
|
||||
// 检测项目类型(只在 verbose 模式下输出,避免重复)
|
||||
const projectType = detectProjectType();
|
||||
log(`检测到项目类型: ${projectType}`, verbose);
|
||||
|
||||
// 对于现代框架,使用智能注入
|
||||
if (
|
||||
['vite', 'next-app', 'next-pages', 'next-hybrid'].includes(projectType)
|
||||
) {
|
||||
try {
|
||||
const success = smartInjectFramework({ remote, dryRun, verbose });
|
||||
// smartInjectFramework 内部已经输出了所有必要的日志,这里不需要重复输出
|
||||
return success;
|
||||
} catch (error) {
|
||||
logError(`框架注入失败: ${error.message}`);
|
||||
logInfo('回退到传统 HTML 注入模式...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 传统 HTML 注入模式(兜底方案)
|
||||
logInfo('使用传统 HTML 注入模式...');
|
||||
|
||||
// 查找 HTML 文件
|
||||
const targetFiles = file ? [file] : lookupFiles();
|
||||
|
||||
if (targetFiles.length === 0) {
|
||||
throw new Error('未找到 HTML 文件');
|
||||
}
|
||||
|
||||
log(`找到 ${targetFiles.length} 个 HTML 文件:`, verbose);
|
||||
targetFiles.forEach(f => log(` - ${f}`, verbose));
|
||||
|
||||
// 解析远程脚本类型和生成脚本标签
|
||||
const remoteType = parseRemoteType(remote);
|
||||
const scriptTag = generateScriptTag(remote, remoteType);
|
||||
|
||||
log(`脚本类型: ${remoteType}`, verbose);
|
||||
log(`脚本标签: ${scriptTag}`, verbose);
|
||||
|
||||
// 处理每个 HTML 文件
|
||||
let successCount = 0;
|
||||
|
||||
for (const htmlFile of targetFiles) {
|
||||
try {
|
||||
// 读取 HTML 内容
|
||||
let htmlContent = readFile(htmlFile);
|
||||
const originalContent = htmlContent;
|
||||
|
||||
// 移除之前注入的脚本(避免重复)
|
||||
htmlContent = removeInjectedScripts(htmlContent);
|
||||
|
||||
// 注入新脚本 - 传入 remote URL 以使用动态时间戳
|
||||
const newContent = injectScriptToHtml(htmlContent, scriptTag, 'dev-inject', remote);
|
||||
|
||||
// 检查是否有变化
|
||||
if (newContent === originalContent) {
|
||||
log(`文件 ${htmlFile} 无需更新`, verbose);
|
||||
successCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
if (!dryRun) {
|
||||
writeFile(htmlFile, newContent);
|
||||
logSuccess(`已注入脚本到 ${htmlFile}`);
|
||||
} else {
|
||||
logInfo(`[DRY RUN] 将注入脚本到 ${htmlFile}`);
|
||||
}
|
||||
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
logError(`处理文件 ${htmlFile} 失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
logSuccess(`成功处理 ${successCount} 个文件`);
|
||||
logInfo(`脚本地址: ${remote}`);
|
||||
|
||||
if (remoteType === 'absolute-path') {
|
||||
logInfo('提示:请确保静态文件服务器可以访问该路径');
|
||||
}
|
||||
} else {
|
||||
logError('没有成功处理任何文件');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载脚本从 HTML 文件
|
||||
*/
|
||||
export async function uninstallScript(options = {}) {
|
||||
const { file, dryRun = false, verbose = false, framework } = options;
|
||||
|
||||
// 如果指定了 framework 标志,使用框架感知卸载
|
||||
if (framework) {
|
||||
const { smartUninstall } = await import('./framework-inject.js');
|
||||
return smartUninstall(options);
|
||||
}
|
||||
|
||||
logInfo('开始移除注入的脚本...');
|
||||
|
||||
// 查找 HTML 文件
|
||||
const targetFiles = file ? [file] : lookupFiles();
|
||||
|
||||
if (targetFiles.length === 0) {
|
||||
logInfo('未找到 HTML 文件');
|
||||
return;
|
||||
}
|
||||
|
||||
log(`找到 ${targetFiles.length} 个 HTML 文件:`, verbose);
|
||||
targetFiles.forEach(f => log(` - ${f}`, verbose));
|
||||
|
||||
// 处理每个 HTML 文件
|
||||
let successCount = 0;
|
||||
|
||||
for (const htmlFile of targetFiles) {
|
||||
try {
|
||||
// 读取 HTML 内容
|
||||
let htmlContent = readFile(htmlFile);
|
||||
const originalContent = htmlContent;
|
||||
|
||||
// 移除注入的脚本
|
||||
const newContent = removeInjectedScripts(htmlContent);
|
||||
|
||||
// 检查是否有变化
|
||||
if (newContent === originalContent) {
|
||||
log(`文件 ${htmlFile} 无需清理`, verbose);
|
||||
successCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
if (!dryRun) {
|
||||
writeFile(htmlFile, newContent);
|
||||
logSuccess(`已从 ${htmlFile} 移除注入的脚本`);
|
||||
} else {
|
||||
logInfo(`[DRY RUN] 将从 ${htmlFile} 移除注入的脚本`);
|
||||
}
|
||||
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
logError(`处理文件 ${htmlFile} 失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (successCount > 0) {
|
||||
logSuccess(`成功清理 ${successCount} 个文件`);
|
||||
} else {
|
||||
logInfo('没有找到需要清理的注入脚本');
|
||||
}
|
||||
}
|
||||
275
qiming-dev-inject/lib/utils.js
Normal file
275
qiming-dev-inject/lib/utils.js
Normal file
@@ -0,0 +1,275 @@
|
||||
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 `<script src="${remote}${separator}t=${timestamp}"></script>`;
|
||||
} else if (type === 'absolute-path') {
|
||||
return `<script src="${remote}${separator}t=${timestamp}"></script>`;
|
||||
} else {
|
||||
throw new Error(`未知的远程类型: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 HTML 内容中移除注入的脚本 - 同时清理前后的空白行
|
||||
*/
|
||||
export function removeInjectedScripts(htmlContent, identifier = 'dev-inject') {
|
||||
// 使用标准的开始/结束标识移除注入的代码
|
||||
// 匹配包括前后换行符和空白在内的完整块
|
||||
// 使用非贪婪匹配确保只匹配一个注入块
|
||||
const injectBlockRegex = /(\r?\n)?\s*<!-- DEV-INJECT-START -->[\s\S]*?<!-- DEV-INJECT-END -->\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 '';
|
||||
});
|
||||
|
||||
// 清理移除后可能产生的多余空白行
|
||||
// 清理 <head> 标签后的多余空白行(保留一个换行)
|
||||
cleaned = cleaned.replace(/(<head[^>]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2');
|
||||
// 清理 </head> 前的多余空白行(保留一个换行)
|
||||
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 = `<!-- DEV-INJECT-START -->
|
||||
<script data-id="${scriptId}">
|
||||
(function() {
|
||||
const remote = ${safeRemote};
|
||||
const separator = remote.includes('?') ? '&' : '?';
|
||||
const script = document.createElement('script');
|
||||
script.src = remote + separator + 't=' + Date.now();
|
||||
script.dataset.id = '${scriptId}-script';
|
||||
script.defer = true;
|
||||
// 防止重复注入
|
||||
if (!document.querySelector('[data-id="${scriptId}-script"]')) {
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!-- DEV-INJECT-END -->`;
|
||||
} else {
|
||||
// 向后兼容:从 scriptTag 中提取 URL 并添加时间戳
|
||||
const scriptSrc = scriptTag.match(/src="([^"]*)"/)?.[1] || '';
|
||||
if (scriptSrc) {
|
||||
// 提取基础 URL(移除可能已存在的时间戳参数)
|
||||
const baseUrl = scriptSrc.split('?')[0];
|
||||
const separator = scriptSrc.includes('?') ? '&' : '?';
|
||||
scriptContent = `<!-- DEV-INJECT-START -->
|
||||
<script>
|
||||
(function() {
|
||||
const remote = ${JSON.stringify(baseUrl)};
|
||||
const separator = remote.includes('?') ? '&' : '?';
|
||||
const script = document.createElement('script');
|
||||
script.src = remote + separator + 't=' + Date.now();
|
||||
script.defer = true;
|
||||
// 防止重复注入
|
||||
if (!document.querySelector('script[src*="' + remote.split('/').pop() + '"]')) {
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!-- DEV-INJECT-END -->`;
|
||||
} else {
|
||||
// 如果无法提取 URL,使用旧的静态方式
|
||||
scriptContent = `<!-- DEV-INJECT-START -->
|
||||
${scriptTag}
|
||||
<!-- DEV-INJECT-END -->`;
|
||||
}
|
||||
}
|
||||
|
||||
// 查找 </head> 标签
|
||||
const headEndIndex = htmlContent.lastIndexOf('</head>');
|
||||
|
||||
if (headEndIndex !== -1) {
|
||||
// 在 </head> 之前注入
|
||||
const beforeHead = htmlContent.substring(0, headEndIndex);
|
||||
const afterHead = htmlContent.substring(headEndIndex);
|
||||
|
||||
// 检查 </head> 前是否有非空白内容
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理可能的多余空白行(保留最多一个空行)
|
||||
// 清理 <head> 标签后的多余空白行
|
||||
result = result.replace(/(<head[^>]*>)\s*\n\s*\n+(\s*)/g, '$1\n$2');
|
||||
// 清理 </head> 前的多余空白行
|
||||
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 {
|
||||
// 如果没有找到 </head>,查找 <body> 标签
|
||||
const bodyStartIndex = htmlContent.indexOf('<body');
|
||||
|
||||
if (bodyStartIndex !== -1) {
|
||||
const bodyEndIndex = 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}`);
|
||||
}
|
||||
Reference in New Issue
Block a user