提交qiming-dev-inject

This commit is contained in:
Codex
2026-06-01 12:53:19 +08:00
parent 96af3b324b
commit 9e9486b7c2
76 changed files with 6397 additions and 0 deletions

View 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;
}