const { resolve } = require('path') const { readFileSync, existsSync, readdirSync } = require('fs') const fs = require('fs') const parser = require('@babel/parser') const traverse = require('@babel/traverse').default const { parse } = require('@vue/compiler-sfc') // 第一阶段路由映射插件 - 收集直接映射关系 function routeMappingPlugin() { // 检查是否在静默模式下运行 const isSilentMode = process.env.ROUTE_MAPPING_SILENT === 'true' || process.argv.includes('--silent') // 日志函数 const log = (message) => { if (!isSilentMode) { console.log(message) } } return { name: 'route-mapping-phase1', // 防重复生成机制 _lastGenerationTime: 0, _generationInProgress: false, _generationCooldown: 10000, // 10秒冷却时间,增加冷却时间 _maxGenerationsPerMinute: 3, // 每分钟最多生成3次 _generationCount: 0, _generationWindowStart: 0, // Webpack插件接口 apply(compiler) { const self = this // 在编译开始前就生成映射文件 compiler.hooks.beforeCompile.tapAsync('RouteMappingPlugin', (params, callback) => { if (self._shouldSkipGeneration()) { log('⏭️ 跳过路由映射生成(冷却期内)') callback() return } log('🔧 第一阶段:开始收集直接路由-API映射关系...') try { self.collectDirectMappings() callback() } catch (error) { console.error('❌ 路由映射插件执行失败:', error) callback(error) } }) // 备用钩子,确保在beforeRun时也执行 compiler.hooks.beforeRun.tapAsync('RouteMappingPlugin', (compilation, callback) => { if (self._shouldSkipGeneration()) { console.log('⏭️ 跳过路由映射生成(冷却期内)') callback() return } console.log('🔧 第一阶段:开始收集直接路由-API映射关系...') try { self.collectDirectMappings() callback() } catch (error) { console.error('❌ 路由映射插件执行失败:', error) callback(error) } }) }, // Vite插件接口 buildStart() { console.log('🔧 第一阶段:开始收集直接路由-API映射关系...') this.collectDirectMappings() }, // 在开发模式下也执行 configureServer(server) { if (this._shouldSkipGeneration()) { console.log('⏭️ 跳过路由映射生成(冷却期内)') return } console.log('🔧 开发模式下收集直接映射关系...') this.collectDirectMappings() }, // 检查是否应该跳过生成 _shouldSkipGeneration() { const now = Date.now() // 如果正在生成中,跳过 if (this._generationInProgress) { console.log('⏭️ 跳过生成:正在生成中') return true } // 如果在冷却期内,跳过 if (now - this._lastGenerationTime < this._generationCooldown) { const remainingTime = Math.ceil((this._generationCooldown - (now - this._lastGenerationTime)) / 1000) console.log(`⏭️ 跳过生成:冷却期内,还需等待 ${remainingTime} 秒`) return true } // 检查每分钟生成次数限制 if (now - this._generationWindowStart > 60000) { // 重置计数窗口 this._generationCount = 0 this._generationWindowStart = now } if (this._generationCount >= this._maxGenerationsPerMinute) { console.log(`⏭️ 跳过生成:已达到每分钟最大生成次数限制 (${this._maxGenerationsPerMinute} 次)`) return true } return false }, // 检查是否需要重新生成文件 _shouldRegenerateFile() { const outputPath = resolve(__dirname, '../src/renderer/modules/route-sync/direct-route-mappings.js') // 如果文件不存在,需要生成 if (!existsSync(outputPath)) { console.log('📄 映射文件不存在,需要生成') return true } const outputStats = fs.statSync(outputPath) const currentTime = Date.now() const fileModifyTime = outputStats.mtime.getTime() const timeDifference = currentTime - fileModifyTime const thirtyMinutes = 30 * 60 * 1000 // 30分钟的毫秒数 // 检查文件是否超过30分钟未更新 if (timeDifference > thirtyMinutes) { console.log(`⏰ 映射文件已存在 ${Math.round(timeDifference / (60 * 1000))} 分钟,超过30分钟限制,需要重新生成`) return true } // 检查源文件是否比生成文件更新 const sourceFiles = this._getSourceFiles() for (const sourceFile of sourceFiles) { if (existsSync(sourceFile)) { const sourceStats = fs.statSync(sourceFile) if (sourceStats.mtime > outputStats.mtime) { console.log(`📝 检测到源文件更新: ${sourceFile}`) return true } } } console.log(`✅ 映射文件无需重新生成 (文件年龄: ${Math.round(timeDifference / (60 * 1000))} 分钟)`) return false }, // 获取需要监控的源文件列表 _getSourceFiles() { const sourceFiles = [] // 路由配置文件 sourceFiles.push(resolve(__dirname, '../src/renderer/router/index.js')) // 模块配置文件 sourceFiles.push(resolve(__dirname, '../src/renderer/modules/config.js')) // 页面组件文件 const moduleDirs = this.readModuleNamesFromConfig() moduleDirs.forEach(moduleName => { // 页面组件 const viewsPath = resolve(__dirname, `../src/renderer/modules/${moduleName}/views`) if (existsSync(viewsPath)) { const files = readdirSync(viewsPath).filter(f => f.endsWith('.vue')) files.forEach(file => { sourceFiles.push(resolve(viewsPath, file)) }) } // 弹窗组件 const componentsPath = resolve(__dirname, `../src/renderer/modules/${moduleName}/components`) if (existsSync(componentsPath)) { const files = readdirSync(componentsPath).filter(f => f.endsWith('.vue') && (f.toLowerCase().includes('modal') || f.toLowerCase().includes('dialog')) ) files.forEach(file => { sourceFiles.push(resolve(componentsPath, file)) }) } }) return sourceFiles }, // 收集直接映射关系 collectDirectMappings() { // 设置生成中标志 this._generationInProgress = true try { // 检查文件是否需要重新生成 if (this._shouldRegenerateFile()) { console.log('🔄 检测到需要重新生成路由映射文件') // 1. 分析路由配置 const routes = this.analyzeRoutes() // 2. 收集API信息(第一层) const apiMappings = this.collectApiMappings(routes) // 3. 关联页面与API(第三层) - 启用并调整逻辑 const enhancedApiMappings = this.associatePagesWithApi(routes, apiMappings) // 4. 清理和优化API映射(第四层) - 启用并调整逻辑 const optimizedApiMappings = this.optimizeApiMappings(routes, enhancedApiMappings) // 5. 生成最终映射文件 this.generateMappingFile(routes, optimizedApiMappings) console.log('✅ 第一阶段直接映射关系收集完成') } else { console.log('⏭️ 路由映射文件无需重新生成') } } catch (error) { console.error('❌ 直接映射关系收集失败:', error) throw error } finally { // 重置生成中标志并更新时间戳 this._generationInProgress = false this._lastGenerationTime = Date.now() this._generationCount++ if (this._generationWindowStart === 0) { this._generationWindowStart = Date.now() } } }, // 分析路由配置 analyzeRoutes() { const routerPath = resolve(__dirname, '../src/renderer/router/index.js') if (!existsSync(routerPath)) { throw new Error('Router file not found') } const routerContent = readFileSync(routerPath, 'utf-8') const ast = parser.parse(routerContent, { sourceType: 'module', plugins: ['jsx'] }) const routes = [] const self = this traverse(ast, { ObjectExpression(path) { const properties = path.node.properties let route = {} properties.forEach(prop => { if (prop.key && prop.key.name === 'path' && prop.value && prop.value.value) { route.path = prop.value.value } if (prop.key && prop.key.name === 'name' && prop.value && prop.value.value) { route.name = prop.value.value } if (prop.key && prop.key.name === 'component' && prop.value) { route.component = self.extractComponentName(prop.value) } }) if (route.path) { route.module = self.extractModuleFromPath(route.path) // 添加description属性 route.description = self.getRouteDescription(route.module, route.path) routes.push(route) } } }) return routes }, // 分析单个服务文件的API映射 analyzeServiceFileForApiMappings(servicePath, serviceName, moduleName) { try { const content = readFileSync(servicePath, 'utf-8') const ast = parser.parse(content, { sourceType: 'module', plugins: ['jsx'], attachComments: true }) const apiMappings = [] const self = this traverse(ast, { // 查找函数定义 FunctionDeclaration(path) { const functionName = path.node.id?.name if (functionName) { // 分析函数内部的API调用 const apiCalls = self.extractApiCallsFromFunction(path) apiCalls.forEach(apiCall => { apiMappings.push({ apiMethodName: functionName, method: apiCall.method, path: apiCall.path, line: apiCall.line }) }) } }, // 查找箭头函数和函数表达式 VariableDeclarator(path) { if (path.node.init && (path.node.init.type === 'ArrowFunctionExpression' || path.node.init.type === 'FunctionExpression')) { const functionName = path.node.id?.name if (functionName) { const functionPath = path.get('init') const apiCalls = self.extractApiCallsFromFunction(functionPath) apiCalls.forEach(apiCall => { apiMappings.push({ apiMethodName: functionName, method: apiCall.method, path: apiCall.path, line: apiCall.line }) }) } } }, // 查找对象方法定义 ObjectMethod(path) { const apiMethodName = path.node.key?.name if (apiMethodName) { const apiCalls = self.extractApiCallsFromFunction(path) apiCalls.forEach(apiCall => { apiMappings.push({ apiMethodName: apiMethodName, method: apiCall.method, path: apiCall.path, line: apiCall.line }) }) } }, // 查找对象属性中的函数 ObjectProperty(path) { if (path.node.value && (path.node.value.type === 'ArrowFunctionExpression' || path.node.value.type === 'FunctionExpression')) { const apiMethodName = path.node.key?.name if (apiMethodName) { const apiCalls = self.extractApiCallsFromFunction(path.get('value')) apiCalls.forEach(apiCall => { apiMappings.push({ apiMethodName: apiMethodName, method: apiCall.method, path: apiCall.path, line: apiCall.line }) }) } } } }) return apiMappings } catch (error) { console.warn(`⚠️ 分析服务文件失败: ${servicePath}`, error.message) return [] } }, // 从函数中提取API调用 extractApiCallsFromFunction(functionPath) { const apiCalls = [] const self = this // 首先检查函数注释中是否有 @serverApiMethod 标记 const commentApiInfo = self.extractApiFromComment(functionPath) if (commentApiInfo) { apiCalls.push(commentApiInfo) return apiCalls } // 如果没有注释信息,则使用原来的AST遍历方式 if (functionPath.node) { traverse(functionPath.node, { CallExpression(path) { const callInfo = self.extractApiCall(path) if (callInfo && callInfo.path) { apiCalls.push(callInfo) } } }, functionPath.scope, functionPath) } return apiCalls }, // 从注释中提取API信息 extractApiFromComment(functionPath) { const node = functionPath.node // 首先检查 leadingComments if (node && node.leadingComments) { for (const comment of node.leadingComments) { const commentText = comment.value.trim() const match = commentText.match(/@serverApiMethod\s+(\w+)\s+(.+)/) if (match) { const method = match[1].toUpperCase() const path = match[2].trim() return { method, path, line: node.loc ? node.loc.start.line : 0, source: 'comment' } } } } else { } // 如果没有找到注释,尝试从函数名推断API信息 // 这是一个备用方案,用于测试 return null }, // 第二步(第一层):收集API信息 collectApiMappings(routes) { const apiMappings = [] const moduleDirs = this.readModuleNamesFromConfig() console.log('🔍 开始收集API映射,模块列表:', moduleDirs) moduleDirs.forEach(moduleName => { const servicesPath = resolve(__dirname, `../src/renderer/modules/${moduleName}/services`) if (existsSync(servicesPath)) { const serviceFiles = readdirSync(servicesPath).filter(f => f.endsWith('.js')) console.log(`🔍 模块 ${moduleName} 的service文件:`, serviceFiles) serviceFiles.forEach(serviceFile => { const serviceName = serviceFile.replace('.js', '') const servicePath = resolve(servicesPath, serviceFile) const serviceApiMappings = this.analyzeServiceFileForApiMappings(servicePath, serviceName, moduleName) console.log(`🔍 服务 ${serviceName} 的API映射数量:`, serviceApiMappings.length) if (serviceApiMappings.length > 0) { apiMappings.push({ module: moduleName, serviceName: serviceName, servicePath: servicePath, apiMappings: serviceApiMappings }) } }) } else { console.warn(`⚠️ 模块 ${moduleName} 的services目录不存在: ${servicesPath}`) } }) console.log('🔍 最终收集到的API映射数量:', apiMappings.length) return apiMappings }, // 第三步:关联页面与API associatePagesWithApi(routes, apiMappings) { console.log('🔍 开始关联页面与API,输入API映射数量:', apiMappings.length) // 为每个API映射添加调用该API的页面信息和触发器信息 const enhancedApiMappings = apiMappings.map(moduleMapping => { console.log(`🔍 处理模块 ${moduleMapping.module},API映射数量:`, moduleMapping.apiMappings.length) const enhancedApiMappings = moduleMapping.apiMappings.map(apiMapping => { // 查找调用该API方法的组件,并收集triggerSources信息 const triggerSources = this.findTriggerSourcesForApiMethod( routes, moduleMapping.module, moduleMapping.serviceName, apiMapping.apiMethodName ) console.log(`🔍 API方法 ${apiMapping.apiMethodName} 的触发器数量:`, triggerSources.length) return { ...apiMapping, triggerSources: triggerSources } }) return { ...moduleMapping, apiMappings: enhancedApiMappings } }) console.log('🔍 关联完成,输出API映射数量:', enhancedApiMappings.length) return enhancedApiMappings }, // 查找API方法的触发器源 - 使用简化的方法 findTriggerSourcesForApiMethod(routes, moduleName, serviceName, apiMethodName) { const triggerSources = [] console.log(`🔍 查找API方法 ${serviceName}.${apiMethodName} 的触发器源,模块: ${moduleName}`) // 1. 遍历所有路由,查找调用该API方法的页面组件 routes.forEach(route => { if (route.module === moduleName) { console.log(`🔍 检查路由组件: ${route.component}`) const triggerAnalysis = this.analyzeComponentForTriggerSourcesSimple( route.component, route.path, moduleName, serviceName, apiMethodName ) if (triggerAnalysis && triggerAnalysis.triggerSources.length > 0) { triggerSources.push(...triggerAnalysis.triggerSources) console.log(`✅ 在组件 ${route.component} 中找到 ${triggerAnalysis.triggerSources.length} 个触发器`) } } }) // 2. 搜索模块的components目录,查找调用该API方法的组件 const componentsInModule = this.findComponentsInModule(moduleName) console.log(`🔍 模块 ${moduleName} 的组件:`, componentsInModule) componentsInModule.forEach(componentName => { console.log(`🔍 检查模块组件: ${componentName}`) const triggerAnalysis = this.analyzeComponentForTriggerSourcesSimple( componentName, null, // components目录中的组件没有路由路径 moduleName, serviceName, apiMethodName ) if (triggerAnalysis && triggerAnalysis.triggerSources.length > 0) { triggerSources.push(...triggerAnalysis.triggerSources) console.log(`✅ 在组件 ${componentName} 中找到 ${triggerAnalysis.triggerSources.length} 个触发器`) } }) // 去重 const uniqueTriggerSources = [] const seen = new Set() triggerSources.forEach(trigger => { const key = `${trigger.component}#${trigger.triggerName || 'default'}` if (!seen.has(key)) { seen.add(key) uniqueTriggerSources.push(trigger) } }) console.log(`🔍 API方法 ${serviceName}.${apiMethodName} 最终触发器数量:`, uniqueTriggerSources.length) return uniqueTriggerSources }, // 简化的组件触发器源分析方法 analyzeComponentForTriggerSourcesSimple(componentName, componentPath, moduleName, serviceName, apiMethodName) { try { let filePath = componentPath console.log(`🔍 简化分析组件 ${componentName},查找服务 ${serviceName}.${apiMethodName}`) // 如果是组件名,需要找到对应的文件 if (!filePath || !existsSync(filePath)) { filePath = this.findComponentFile(componentName) } if (!filePath || !existsSync(filePath)) { console.warn(`⚠️ 组件文件未找到: ${componentName}`) return { triggerSources: [] } } console.log(`🔍 找到组件文件: ${filePath}`) const content = readFileSync(filePath, 'utf-8') // 使用AST分析方法 return this.analyzeComponentWithAST(content, componentName, serviceName, apiMethodName, filePath) } catch (error) { console.warn(`⚠️ 简化分析组件触发器源失败: ${componentName}`, error.message) return { triggerSources: [] } } }, // 生成唯一的按钮名称 generateUniqueButtonName(componentName, clickHandler) { // 基于组件名生成简洁的名称 const baseName = componentName.toLowerCase() // 添加随机尾缀确保唯一性 const randomSuffix = Math.random().toString(36).substring(2, 8) return `${baseName}-${randomSuffix}` }, // 为按钮添加name属性 addNameAttributeToButton(filePath, buttonHtml, generatedName) { try { const fs = require('fs') const content = fs.readFileSync(filePath, 'utf-8') // 检查按钮是否已经有name属性 if (buttonHtml.includes('name=')) { console.log(`⚠️ 按钮已有name属性,跳过: ${generatedName}`) return } // 在button标签中添加name属性 const updatedButtonHtml = buttonHtml.replace(/]*)>/, ``) // 替换文件中的按钮HTML const updatedContent = content.replace(buttonHtml, updatedButtonHtml) // 写回文件 fs.writeFileSync(filePath, updatedContent, 'utf-8') console.log(`✅ 已为按钮添加name属性: ${generatedName}`) } catch (error) { console.warn(`⚠️ 添加name属性失败: ${error.message}`) } }, // 使用AST分析组件 analyzeComponentWithAST(content, componentName, serviceName, apiMethodName, filePath = null) { const triggerSources = [] console.log(`🔍 AST分析组件 ${componentName},查找 ${serviceName}.${apiMethodName}`) // 1. 检查组件的authType属性,如果是public则跳过 // 支持两种格式: