851 lines
28 KiB
JavaScript
851 lines
28 KiB
JavaScript
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;
|
||
}
|