Files
qiming/qiming-dev-inject/lib/framework-inject.js
2026-06-01 12:53:19 +08:00

851 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}