提交qiming-dev-inject
This commit is contained in:
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;
|
||||
}
|
||||
Reference in New Issue
Block a user