|
|
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() { |
|
|
return { |
|
|
name: 'route-mapping-phase1', |
|
|
|
|
|
// 防重复生成机制 |
|
|
_lastGenerationTime: 0, |
|
|
_generationInProgress: false, |
|
|
_generationCooldown: 5000, // 5秒冷却时间 |
|
|
|
|
|
// Webpack插件接口 |
|
|
apply(compiler) { |
|
|
const self = this |
|
|
|
|
|
// 在编译开始前就生成映射文件 |
|
|
compiler.hooks.beforeCompile.tapAsync('RouteMappingPlugin', (params, callback) => { |
|
|
if (self._shouldSkipGeneration()) { |
|
|
console.log('⏭️ 跳过路由映射生成(冷却期内)') |
|
|
callback() |
|
|
return |
|
|
} |
|
|
|
|
|
console.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) { |
|
|
return true |
|
|
} |
|
|
|
|
|
// 如果在冷却期内,跳过 |
|
|
if (now - this._lastGenerationTime < this._generationCooldown) { |
|
|
return true |
|
|
} |
|
|
|
|
|
return false |
|
|
}, |
|
|
|
|
|
// 检查是否需要重新生成文件 |
|
|
_shouldRegenerateFile() { |
|
|
const outputPath = resolve(__dirname, '../src/renderer/modules/route-sync/direct-route-mappings.js') |
|
|
|
|
|
// 如果文件不存在,需要生成 |
|
|
if (!existsSync(outputPath)) { |
|
|
return true |
|
|
} |
|
|
|
|
|
// 检查源文件是否比生成文件更新 |
|
|
const sourceFiles = this._getSourceFiles() |
|
|
const outputStats = fs.statSync(outputPath) |
|
|
|
|
|
for (const sourceFile of sourceFiles) { |
|
|
if (existsSync(sourceFile)) { |
|
|
const sourceStats = fs.statSync(sourceFile) |
|
|
if (sourceStats.mtime > outputStats.mtime) { |
|
|
return true |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
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. 分析组件关联关系并完善路径信息(第五层) |
|
|
const { componentRelationships, enhancedApiMappings: finalApiMappings } = this.analyzeComponentRelationships(routes, optimizedApiMappings) |
|
|
|
|
|
// 6. 生成最终映射文件 |
|
|
this.generateTestFile(routes, finalApiMappings) |
|
|
|
|
|
console.log('✅ 第一阶段直接映射关系收集完成') |
|
|
} else { |
|
|
console.log('⏭️ 路由映射文件无需重新生成') |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('❌ 直接映射关系收集失败:', error) |
|
|
throw error |
|
|
} finally { |
|
|
// 重置生成中标志并更新时间戳 |
|
|
this._generationInProgress = false |
|
|
this._lastGenerationTime = 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'] |
|
|
}) |
|
|
|
|
|
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({ |
|
|
methodName: 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 apiCalls = self.extractApiCallsFromFunction(path.get('init')) |
|
|
apiCalls.forEach(apiCall => { |
|
|
apiMappings.push({ |
|
|
methodName: functionName, |
|
|
method: apiCall.method, |
|
|
path: apiCall.path, |
|
|
line: apiCall.line |
|
|
}) |
|
|
}) |
|
|
} |
|
|
} |
|
|
}, |
|
|
|
|
|
// 查找对象方法定义 |
|
|
ObjectMethod(path) { |
|
|
const methodName = path.node.key?.name |
|
|
if (methodName) { |
|
|
const apiCalls = self.extractApiCallsFromFunction(path) |
|
|
apiCalls.forEach(apiCall => { |
|
|
apiMappings.push({ |
|
|
methodName: methodName, |
|
|
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 methodName = path.node.key?.name |
|
|
if (methodName) { |
|
|
const apiCalls = self.extractApiCallsFromFunction(path.get('value')) |
|
|
apiCalls.forEach(apiCall => { |
|
|
apiMappings.push({ |
|
|
methodName: methodName, |
|
|
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 |
|
|
|
|
|
// 使用正确的traverse调用方式 |
|
|
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信息 |
|
|
collectApiMappings(routes) { |
|
|
const apiMappings = [] |
|
|
const moduleDirs = this.readModuleNamesFromConfig() |
|
|
|
|
|
moduleDirs.forEach(moduleName => { |
|
|
const servicesPath = resolve(__dirname, `../src/renderer/modules/${moduleName}/services`) |
|
|
|
|
|
if (existsSync(servicesPath)) { |
|
|
const serviceFiles = readdirSync(servicesPath).filter(f => f.endsWith('.js')) |
|
|
|
|
|
serviceFiles.forEach(serviceFile => { |
|
|
const serviceName = serviceFile.replace('.js', '') |
|
|
const servicePath = resolve(servicesPath, serviceFile) |
|
|
const serviceApiMappings = this.analyzeServiceFileForApiMappings(servicePath, serviceName, moduleName) |
|
|
|
|
|
if (serviceApiMappings.length > 0) { |
|
|
apiMappings.push({ |
|
|
module: moduleName, |
|
|
serviceName: serviceName, |
|
|
servicePath: servicePath, |
|
|
apiMappings: serviceApiMappings |
|
|
}) |
|
|
} |
|
|
}) |
|
|
} else { |
|
|
console.warn(`⚠️ 模块 ${moduleName} 的services目录不存在: ${servicesPath}`) |
|
|
} |
|
|
}) |
|
|
|
|
|
return apiMappings |
|
|
}, |
|
|
|
|
|
// 第三步:关联页面与API |
|
|
associatePagesWithApi(routes, apiMappings) { |
|
|
|
|
|
// 为每个API映射添加调用该API的页面信息 |
|
|
const enhancedApiMappings = apiMappings.map(moduleMapping => { |
|
|
const enhancedApiMappings = moduleMapping.apiMappings.map(apiMapping => { |
|
|
// 查找调用该API方法的组件 |
|
|
const callingComponents = this.findComponentsCallingApiMethod( |
|
|
routes, |
|
|
moduleMapping.module, |
|
|
moduleMapping.serviceName, |
|
|
apiMapping.methodName |
|
|
) |
|
|
|
|
|
return { |
|
|
...apiMapping, |
|
|
callingComponents: callingComponents |
|
|
} |
|
|
}) |
|
|
|
|
|
return { |
|
|
...moduleMapping, |
|
|
apiMappings: enhancedApiMappings |
|
|
} |
|
|
}) |
|
|
|
|
|
return enhancedApiMappings |
|
|
}, |
|
|
|
|
|
// 查找调用特定API方法的组件 |
|
|
findComponentsCallingApiMethod(routes, moduleName, serviceName, methodName) { |
|
|
const callingComponents = [] |
|
|
|
|
|
// 1. 遍历所有路由,查找调用该API方法的页面组件 |
|
|
routes.forEach(route => { |
|
|
if (route.module === moduleName) { |
|
|
const componentAnalysis = this.analyzeComponentForServiceCalls( |
|
|
route.component, |
|
|
route.path, |
|
|
moduleName |
|
|
) |
|
|
|
|
|
if (componentAnalysis && componentAnalysis.serviceCalls) { |
|
|
// 检查是否有调用该服务方法的调用 |
|
|
const hasServiceCall = componentAnalysis.serviceCalls.some(serviceCall => |
|
|
serviceCall.type === 'service' && |
|
|
serviceCall.service === serviceName && |
|
|
serviceCall.method === methodName |
|
|
) |
|
|
|
|
|
if (hasServiceCall) { |
|
|
callingComponents.push(route.component) |
|
|
} |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
// 2. 搜索模块的components目录,查找调用该API方法的组件 |
|
|
const componentsInModule = this.findComponentsInModule(moduleName) |
|
|
componentsInModule.forEach(componentName => { |
|
|
const componentAnalysis = this.analyzeComponentForServiceCalls( |
|
|
componentName, |
|
|
null, // components目录中的组件没有路由路径 |
|
|
moduleName |
|
|
) |
|
|
|
|
|
if (componentAnalysis && componentAnalysis.serviceCalls) { |
|
|
// 检查是否有调用该服务方法的调用 |
|
|
const hasServiceCall = componentAnalysis.serviceCalls.some(serviceCall => |
|
|
serviceCall.type === 'service' && |
|
|
serviceCall.service === serviceName && |
|
|
serviceCall.method === methodName |
|
|
) |
|
|
|
|
|
if (hasServiceCall) { |
|
|
callingComponents.push(componentName) |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
// 去重 |
|
|
return [...new Set(callingComponents)] |
|
|
}, |
|
|
|
|
|
// 查找模块中的所有组件 |
|
|
findComponentsInModule(moduleName) { |
|
|
const components = [] |
|
|
|
|
|
try { |
|
|
// 查找views目录中的组件 |
|
|
const viewsPath = resolve(__dirname, `../src/renderer/modules/${moduleName}/views`) |
|
|
if (existsSync(viewsPath)) { |
|
|
const viewFiles = readdirSync(viewsPath).filter(f => f.endsWith('.vue')) |
|
|
viewFiles.forEach(file => { |
|
|
components.push(file.replace('.vue', '')) |
|
|
}) |
|
|
} |
|
|
|
|
|
// 查找components目录中的组件 |
|
|
const componentsPath = resolve(__dirname, `../src/renderer/modules/${moduleName}/components`) |
|
|
if (existsSync(componentsPath)) { |
|
|
const componentFiles = readdirSync(componentsPath).filter(f => f.endsWith('.vue')) |
|
|
componentFiles.forEach(file => { |
|
|
components.push(file.replace('.vue', '')) |
|
|
}) |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn(`⚠️ 查找模块组件失败: ${moduleName}`, error.message) |
|
|
} |
|
|
|
|
|
return components |
|
|
}, |
|
|
|
|
|
// 第四步:清理和优化API映射 |
|
|
optimizeApiMappings(routes, apiMappings) { |
|
|
|
|
|
const optimizedApiMappings = apiMappings.map(moduleMapping => { |
|
|
// 过滤掉callingComponents为空的API映射 |
|
|
const filteredApiMappings = moduleMapping.apiMappings.filter(apiMapping => { |
|
|
return apiMapping.callingComponents && apiMapping.callingComponents.length > 0 |
|
|
}) |
|
|
|
|
|
// 为每个API映射的callingComponents添加路径信息 |
|
|
const enhancedApiMappings = filteredApiMappings.map(apiMapping => { |
|
|
const enhancedCallingComponents = apiMapping.callingComponents.map(componentName => { |
|
|
// 在routes中查找匹配的组件 |
|
|
const matchingRoute = routes.find(route => route.component === componentName) |
|
|
|
|
|
if (matchingRoute) { |
|
|
return { |
|
|
component: componentName, |
|
|
path: matchingRoute.path |
|
|
} |
|
|
} else { |
|
|
// 如果找不到匹配的路由,只返回组件名 |
|
|
return { |
|
|
component: componentName, |
|
|
path: null |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
return { |
|
|
...apiMapping, |
|
|
callingComponents: enhancedCallingComponents |
|
|
} |
|
|
}) |
|
|
|
|
|
return { |
|
|
...moduleMapping, |
|
|
apiMappings: enhancedApiMappings |
|
|
} |
|
|
}).filter(moduleMapping => moduleMapping.apiMappings.length > 0) // 过滤掉没有API映射的模块 |
|
|
|
|
|
const totalRemoved = apiMappings.reduce((sum, module) => |
|
|
sum + module.apiMappings.length, 0) - optimizedApiMappings.reduce((sum, module) => |
|
|
sum + module.apiMappings.length, 0) |
|
|
|
|
|
return optimizedApiMappings |
|
|
}, |
|
|
|
|
|
// 第五步:分析组件关联关系并完善路径信息 |
|
|
analyzeComponentRelationships(routes, apiMappings) { |
|
|
|
|
|
// 1. 首先收集组件关联关系 |
|
|
const componentRelationships = [] |
|
|
routes.forEach(route => { |
|
|
if (route.component) { |
|
|
const relationships = this.findComponentRelationships(route.component, route.path, route.module) |
|
|
componentRelationships.push(...relationships) |
|
|
} |
|
|
}) |
|
|
|
|
|
|
|
|
// 2. 完善API映射中的路径信息 |
|
|
const enhancedApiMappings = this.enhanceApiMappingsWithPaths(routes, apiMappings, componentRelationships) |
|
|
|
|
|
|
|
|
return { componentRelationships, enhancedApiMappings } |
|
|
}, |
|
|
|
|
|
// 完善API映射中的路径信息 |
|
|
enhanceApiMappingsWithPaths(routes, apiMappings, componentRelationships) { |
|
|
|
|
|
let enhancedCount = 0 |
|
|
|
|
|
// 遍历每个模块的API映射 |
|
|
const enhancedApiMappings = apiMappings.map(moduleMapping => { |
|
|
const enhancedModuleApiMappings = moduleMapping.apiMappings.map(apiMapping => { |
|
|
const enhancedCallingComponents = apiMapping.callingComponents.map(callingComponent => { |
|
|
// 如果path为空,尝试通过组件关系找到根组件路径 |
|
|
if (callingComponent.path === null) { |
|
|
const rootPath = this.findRootComponentPath( |
|
|
callingComponent.component, |
|
|
componentRelationships, |
|
|
routes |
|
|
) |
|
|
|
|
|
if (rootPath) { |
|
|
enhancedCount++ |
|
|
return { |
|
|
...callingComponent, |
|
|
path: rootPath |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return callingComponent |
|
|
}) |
|
|
|
|
|
return { |
|
|
...apiMapping, |
|
|
callingComponents: enhancedCallingComponents |
|
|
} |
|
|
}) |
|
|
|
|
|
return { |
|
|
...moduleMapping, |
|
|
apiMappings: enhancedModuleApiMappings |
|
|
} |
|
|
}) |
|
|
|
|
|
return enhancedApiMappings |
|
|
}, |
|
|
|
|
|
// 通过组件关系找到根组件路径 |
|
|
findRootComponentPath(targetComponent, componentRelationships, routes) { |
|
|
// 递归查找组件关系链,直到找到根组件 |
|
|
const findParent = (componentName, visited = new Set()) => { |
|
|
// 避免循环引用 |
|
|
if (visited.has(componentName)) { |
|
|
return null |
|
|
} |
|
|
visited.add(componentName) |
|
|
|
|
|
// 查找该组件的父组件 |
|
|
const parentRelationship = componentRelationships.find(rel => |
|
|
rel.toComponent === componentName |
|
|
) |
|
|
|
|
|
if (parentRelationship) { |
|
|
const parentComponent = parentRelationship.fromComponent |
|
|
|
|
|
// 检查父组件是否为路由表中的根组件 |
|
|
const route = routes.find(route => route.component === parentComponent) |
|
|
if (route) { |
|
|
return route.path |
|
|
} |
|
|
|
|
|
// 如果父组件不是根组件,继续向上查找 |
|
|
return findParent(parentComponent, visited) |
|
|
} |
|
|
|
|
|
return null |
|
|
} |
|
|
|
|
|
return findParent(targetComponent) |
|
|
}, |
|
|
|
|
|
// 查找单个组件的关联关系 |
|
|
findComponentRelationships(componentName, componentPath, moduleName) { |
|
|
const relationships = [] |
|
|
|
|
|
try { |
|
|
// 查找组件文件 |
|
|
const componentFile = this.findComponentFile(componentName) |
|
|
if (!componentFile || !existsSync(componentFile)) { |
|
|
return relationships |
|
|
} |
|
|
|
|
|
const content = readFileSync(componentFile, 'utf-8') |
|
|
|
|
|
if (componentFile.endsWith('.vue')) { |
|
|
const { descriptor } = parse(content) |
|
|
const scriptContent = descriptor.script?.content || '' |
|
|
const templateContent = descriptor.template?.content || '' |
|
|
|
|
|
// 分析模板中的组件引用 |
|
|
const templateRelationships = this.analyzeTemplateForComponents(templateContent, componentName) |
|
|
relationships.push(...templateRelationships) |
|
|
|
|
|
// 分析脚本中的组件引用 |
|
|
if (scriptContent) { |
|
|
const scriptRelationships = this.analyzeScriptForComponents(scriptContent, componentName) |
|
|
relationships.push(...scriptRelationships) |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn(`⚠️ 分析组件关联关系失败: ${componentName}`, error.message) |
|
|
} |
|
|
|
|
|
return relationships |
|
|
}, |
|
|
|
|
|
// 分析模板中的组件引用 |
|
|
analyzeTemplateForComponents(templateContent, fromComponent) { |
|
|
const relationships = [] |
|
|
|
|
|
// 查找组件标签(如 <UserRoleAssignment>) |
|
|
const componentTagRegex = /<([A-Z][a-zA-Z0-9]*)/g |
|
|
let match |
|
|
|
|
|
while ((match = componentTagRegex.exec(templateContent)) !== null) { |
|
|
const componentTag = match[1] |
|
|
|
|
|
// 跳过HTML标签 |
|
|
if (this.isHtmlTag(componentTag)) { |
|
|
continue |
|
|
} |
|
|
|
|
|
// 确定关联类型 |
|
|
let relationship = 'embed' // 默认是嵌入关系 |
|
|
|
|
|
// 检查是否是弹窗组件 |
|
|
if (this.isModalComponent(componentTag, templateContent, match.index)) { |
|
|
relationship = 'modal' |
|
|
} |
|
|
|
|
|
relationships.push({ |
|
|
fromComponent: fromComponent, |
|
|
toComponent: componentTag, |
|
|
relationship: relationship |
|
|
}) |
|
|
} |
|
|
|
|
|
return relationships |
|
|
}, |
|
|
|
|
|
// 分析脚本中的组件引用 |
|
|
analyzeScriptForComponents(scriptContent, fromComponent) { |
|
|
const relationships = [] |
|
|
|
|
|
try { |
|
|
const ast = parser.parse(scriptContent, { |
|
|
sourceType: 'module', |
|
|
plugins: ['jsx'] |
|
|
}) |
|
|
|
|
|
const self = this |
|
|
traverse(ast, { |
|
|
// 查找import语句 |
|
|
ImportDeclaration(path) { |
|
|
const source = path.node.source.value |
|
|
if (source.startsWith('./') || source.startsWith('../')) { |
|
|
// 相对路径导入,可能是组件 |
|
|
const importedNames = path.node.specifiers.map(spec => { |
|
|
if (spec.type === 'ImportDefaultSpecifier') { |
|
|
return spec.local.name |
|
|
} else if (spec.type === 'ImportSpecifier') { |
|
|
return spec.imported.name |
|
|
} |
|
|
return null |
|
|
}).filter(Boolean) |
|
|
|
|
|
importedNames.forEach(importedName => { |
|
|
if (self.isComponentName(importedName)) { |
|
|
relationships.push({ |
|
|
fromComponent: fromComponent, |
|
|
toComponent: importedName, |
|
|
relationship: 'import' |
|
|
}) |
|
|
} |
|
|
}) |
|
|
} |
|
|
} |
|
|
}) |
|
|
} catch (error) { |
|
|
console.warn(`⚠️ 分析脚本组件引用失败: ${fromComponent}`, error.message) |
|
|
} |
|
|
|
|
|
return relationships |
|
|
}, |
|
|
|
|
|
// 判断是否是HTML标签 |
|
|
isHtmlTag(tagName) { |
|
|
const htmlTags = [ |
|
|
'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', |
|
|
'button', 'input', 'form', 'table', 'tr', 'td', 'th', |
|
|
'ul', 'ol', 'li', 'a', 'img', 'select', 'option', |
|
|
'textarea', 'label', 'fieldset', 'legend', 'section', |
|
|
'article', 'header', 'footer', 'nav', 'aside', 'main' |
|
|
] |
|
|
return htmlTags.includes(tagName.toLowerCase()) |
|
|
}, |
|
|
|
|
|
// 判断是否是弹窗组件 |
|
|
isModalComponent(componentTag, templateContent, startIndex) { |
|
|
// 查找组件标签周围的上下文 |
|
|
const beforeContext = templateContent.substring(Math.max(0, startIndex - 100), startIndex) |
|
|
const afterContext = templateContent.substring(startIndex, Math.min(templateContent.length, startIndex + 200)) |
|
|
|
|
|
// 检查是否包含弹窗相关的关键词 |
|
|
const modalKeywords = ['modal', 'dialog', 'popup', 'overlay', 'v-if', 'v-show'] |
|
|
const context = (beforeContext + afterContext).toLowerCase() |
|
|
|
|
|
return modalKeywords.some(keyword => context.includes(keyword)) |
|
|
}, |
|
|
|
|
|
// 判断是否是组件名称 |
|
|
isComponentName(name) { |
|
|
// Vue组件通常以大写字母开头 |
|
|
return /^[A-Z]/.test(name) && name.length > 2 |
|
|
}, |
|
|
|
|
|
|
|
|
// 分析组件中的服务调用 |
|
|
analyzeComponentForServiceCalls(componentName, componentPath, moduleName) { |
|
|
try { |
|
|
let filePath = componentPath |
|
|
|
|
|
// 如果是组件名,需要找到对应的文件 |
|
|
if (!filePath || !existsSync(filePath)) { |
|
|
filePath = this.findComponentFile(componentName) |
|
|
} |
|
|
|
|
|
if (!filePath || !existsSync(filePath)) { |
|
|
console.warn(`⚠️ 组件文件未找到: ${componentName}`) |
|
|
return null |
|
|
} |
|
|
|
|
|
const content = readFileSync(filePath, 'utf-8') |
|
|
|
|
|
if (filePath.endsWith('.vue')) { |
|
|
return this.analyzeVueComponentForServiceCalls(content, componentName, moduleName) |
|
|
} else { |
|
|
return this.analyzeJavaScriptComponentForServiceCalls(content, componentName, moduleName) |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn(`⚠️ 分析组件失败: ${componentName}`, error.message) |
|
|
return null |
|
|
} |
|
|
}, |
|
|
|
|
|
// 分析Vue组件中的服务调用 |
|
|
analyzeVueComponentForServiceCalls(content, componentName, moduleName) { |
|
|
const { descriptor } = parse(content) |
|
|
const scriptContent = descriptor.script?.content || '' |
|
|
|
|
|
if (!scriptContent) { |
|
|
return { serviceCalls: [], methods: [] } |
|
|
} |
|
|
|
|
|
const ast = parser.parse(scriptContent, { |
|
|
sourceType: 'module', |
|
|
plugins: ['jsx'] |
|
|
}) |
|
|
|
|
|
return this.extractServiceCallsFromAST(ast, componentName, moduleName) |
|
|
}, |
|
|
|
|
|
// 分析JavaScript组件中的服务调用 |
|
|
analyzeJavaScriptComponentForServiceCalls(content, componentName, moduleName) { |
|
|
const ast = parser.parse(content, { |
|
|
sourceType: 'module', |
|
|
plugins: ['jsx'] |
|
|
}) |
|
|
|
|
|
return this.extractServiceCallsFromAST(ast, componentName, moduleName) |
|
|
}, |
|
|
|
|
|
// 从AST中提取服务调用 |
|
|
extractServiceCallsFromAST(ast, componentName, moduleName) { |
|
|
const serviceCalls = [] |
|
|
const methods = [] |
|
|
|
|
|
const self = this |
|
|
traverse(ast, { |
|
|
// 查找方法定义 |
|
|
FunctionDeclaration(path) { |
|
|
if (path.node.id) { |
|
|
methods.push(path.node.id.name) |
|
|
} |
|
|
}, |
|
|
|
|
|
// 查找箭头函数 |
|
|
ArrowFunctionExpression(path) { |
|
|
if (path.parent && path.parent.type === 'VariableDeclarator' && path.parent.id) { |
|
|
methods.push(path.parent.id.name) |
|
|
} |
|
|
}, |
|
|
|
|
|
// 查找服务调用 |
|
|
CallExpression(path) { |
|
|
const callInfo = self.extractServiceCall(path) |
|
|
if (callInfo) { |
|
|
serviceCalls.push(callInfo) |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
return { serviceCalls, methods } |
|
|
}, |
|
|
|
|
|
// 提取服务调用信息 |
|
|
extractServiceCall(path) { |
|
|
const node = path.node |
|
|
|
|
|
// 检查是否是服务调用 (service.method()) |
|
|
if (node.callee && node.callee.type === 'MemberExpression') { |
|
|
const object = node.callee.object |
|
|
const property = node.callee.property |
|
|
|
|
|
if (object && property && object.name && property.name) { |
|
|
// 检查是否是服务调用 |
|
|
if (object.name.endsWith('Service') || object.name === 'api' || object.name === 'axios') { |
|
|
return { |
|
|
type: 'service', |
|
|
service: object.name, |
|
|
method: property.name, |
|
|
arguments: node.arguments.map(arg => this.extractArgumentValue(arg)), |
|
|
line: node.loc?.start?.line || 0 |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return null |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 提取API调用信息 |
|
|
extractApiCall(path) { |
|
|
const node = path.node |
|
|
const self = this |
|
|
|
|
|
// 检查是否是直接的API调用 (api.get, api.post等) |
|
|
if (node.callee && node.callee.type === 'MemberExpression') { |
|
|
const object = node.callee.object |
|
|
const property = node.callee.property |
|
|
|
|
|
if (object && property) { |
|
|
// 支持多种API调用方式 |
|
|
const apiObjects = ['api', 'axios', 'http', 'request'] |
|
|
const httpMethods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'] |
|
|
|
|
|
// 检查对象名称(可能是Identifier或StringLiteral) |
|
|
let objectName = '' |
|
|
if (object.type === 'Identifier') { |
|
|
objectName = object.name |
|
|
} else if (object.type === 'StringLiteral') { |
|
|
objectName = object.value |
|
|
} |
|
|
|
|
|
// 检查属性名称 |
|
|
let propertyName = '' |
|
|
if (property.type === 'Identifier') { |
|
|
propertyName = property.name |
|
|
} else if (property.type === 'StringLiteral') { |
|
|
propertyName = property.value |
|
|
} |
|
|
|
|
|
if (apiObjects.includes(objectName) && httpMethods.includes(propertyName)) { |
|
|
const method = propertyName |
|
|
const args = node.arguments |
|
|
|
|
|
// 提取API路径 |
|
|
let apiPath = '' |
|
|
if (args.length > 0) { |
|
|
if (args[0].type === 'StringLiteral') { |
|
|
apiPath = args[0].value |
|
|
} else if (args[0].type === 'TemplateLiteral') { |
|
|
// 处理模板字符串 |
|
|
apiPath = self.extractTemplateLiteral(args[0]) |
|
|
} |
|
|
} |
|
|
|
|
|
if (apiPath) { |
|
|
return { |
|
|
method: method.toUpperCase(), |
|
|
path: apiPath, |
|
|
line: node.loc?.start?.line || 0 |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
// 检查是否是服务调用 (service.method()) |
|
|
if (node.callee && node.callee.type === 'MemberExpression') { |
|
|
const object = node.callee.object |
|
|
const property = node.callee.property |
|
|
|
|
|
if (object && property) { |
|
|
let objectName = '' |
|
|
let propertyName = '' |
|
|
|
|
|
if (object.type === 'Identifier') { |
|
|
objectName = object.name |
|
|
} else if (object.type === 'StringLiteral') { |
|
|
objectName = object.value |
|
|
} |
|
|
|
|
|
if (property.type === 'Identifier') { |
|
|
propertyName = property.name |
|
|
} else if (property.type === 'StringLiteral') { |
|
|
propertyName = property.value |
|
|
} |
|
|
|
|
|
// 检查是否是服务调用 |
|
|
if (objectName && propertyName && |
|
|
(objectName.endsWith('Service') || objectName === 'api' || objectName === 'axios')) { |
|
|
return { |
|
|
method: propertyName, |
|
|
service: objectName, |
|
|
line: node.loc?.start?.line || 0 |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return null |
|
|
}, |
|
|
|
|
|
// 提取参数值 |
|
|
extractArgumentValue(node) { |
|
|
if (node.type === 'StringLiteral') { |
|
|
return node.value |
|
|
} else if (node.type === 'NumericLiteral') { |
|
|
return node.value |
|
|
} else if (node.type === 'Identifier') { |
|
|
return node.name |
|
|
} |
|
|
return null |
|
|
}, |
|
|
|
|
|
// 提取模板字符串内容 |
|
|
extractTemplateLiteral(templateLiteral) { |
|
|
if (templateLiteral.type !== 'TemplateLiteral') { |
|
|
return '' |
|
|
} |
|
|
|
|
|
let result = '' |
|
|
templateLiteral.quasis.forEach((quasi, index) => { |
|
|
result += quasi.value.raw |
|
|
if (index < templateLiteral.expressions.length) { |
|
|
// 对于表达式,我们使用占位符 |
|
|
result += `{${templateLiteral.expressions[index].type}}` |
|
|
} |
|
|
}) |
|
|
|
|
|
return result |
|
|
}, |
|
|
|
|
|
// 从模块配置中读取模块名称 |
|
|
readModuleNamesFromConfig() { |
|
|
try { |
|
|
const configPath = resolve(__dirname, '../src/renderer/modules/config.js') |
|
|
if (!existsSync(configPath)) { |
|
|
return [] |
|
|
} |
|
|
|
|
|
const code = readFileSync(configPath, 'utf-8') |
|
|
const ast = parser.parse(code, { sourceType: 'module' }) |
|
|
const modules = [] |
|
|
|
|
|
traverse(ast, { |
|
|
ExportNamedDeclaration(path) { |
|
|
const decl = path.node.declaration |
|
|
if (decl && decl.type === 'VariableDeclaration') { |
|
|
decl.declarations.forEach(d => { |
|
|
if ( |
|
|
d.id && d.id.name === 'MODULE_CONFIG' && |
|
|
d.init && d.init.type === 'ObjectExpression' |
|
|
) { |
|
|
d.init.properties.forEach(prop => { |
|
|
if (prop.type === 'ObjectProperty') { |
|
|
if (prop.key.type === 'Identifier') { |
|
|
modules.push(prop.key.name) |
|
|
} else if (prop.key.type === 'StringLiteral') { |
|
|
modules.push(prop.key.value) |
|
|
} |
|
|
} |
|
|
}) |
|
|
} |
|
|
}) |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
return modules |
|
|
} catch (e) { |
|
|
console.warn('⚠️ 读取 MODULE_CONFIG 失败,使用默认模块集合: ', e.message) |
|
|
return [] |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
// 查找组件文件 |
|
|
findComponentFile(componentName) { |
|
|
// 跳过MainLayout等特殊组件,或者返回正确的路径 |
|
|
if (componentName === 'MainLayout') { |
|
|
// MainLayout在core模块中 |
|
|
const mainLayoutPath = resolve(__dirname, '../src/renderer/modules/core/components/MainLayout.vue') |
|
|
if (existsSync(mainLayoutPath)) { |
|
|
return mainLayoutPath |
|
|
} |
|
|
return null |
|
|
} |
|
|
|
|
|
const moduleDirs = this.readModuleNamesFromConfig() |
|
|
|
|
|
for (const moduleName of moduleDirs) { |
|
|
// 查找views目录 |
|
|
const viewsPath = resolve(__dirname, `../src/renderer/modules/${moduleName}/views`) |
|
|
if (existsSync(viewsPath)) { |
|
|
const files = readdirSync(viewsPath).filter(f => f.endsWith('.vue')) |
|
|
const matchingFile = files.find(f => f.replace('.vue', '') === componentName) |
|
|
if (matchingFile) { |
|
|
return resolve(viewsPath, matchingFile) |
|
|
} |
|
|
} |
|
|
|
|
|
// 查找components目录 |
|
|
const componentsPath = resolve(__dirname, `../src/renderer/modules/${moduleName}/components`) |
|
|
if (existsSync(componentsPath)) { |
|
|
const files = readdirSync(componentsPath).filter(f => f.endsWith('.vue')) |
|
|
const matchingFile = files.find(f => f.replace('.vue', '') === componentName) |
|
|
if (matchingFile) { |
|
|
return resolve(componentsPath, matchingFile) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return null |
|
|
}, |
|
|
|
|
|
// 生成最终映射文件 |
|
|
generateTestFile(routes, apiMappings) { |
|
|
const outputPath = resolve(__dirname, '../src/renderer/modules/route-sync/direct-route-mappings.js') |
|
|
|
|
|
const testContent = `// 直接路由映射文件 |
|
|
// 此文件由 route-mapping-plugin 在构建时生成 |
|
|
// 包含路由配置分析结果和API收集结果 |
|
|
// 请勿手动修改 |
|
|
|
|
|
export const directRouteMappings = { |
|
|
// 路由配置分析结果 |
|
|
routes: ${JSON.stringify(routes, null, 2)}, |
|
|
|
|
|
// API收集结果(包含页面关联和路径完善) |
|
|
apiMappings: ${JSON.stringify(apiMappings, null, 2)} |
|
|
} |
|
|
|
|
|
export default directRouteMappings |
|
|
` |
|
|
|
|
|
fs.writeFileSync(outputPath, testContent, 'utf-8') |
|
|
|
|
|
console.log(`✅ 映射文件已生成: ${outputPath}`) |
|
|
console.log(`📊 统计信息:`) |
|
|
console.log(` - 路由数量: ${routes.length} 个`) |
|
|
console.log(` - 模块数量: ${apiMappings.length} 个`) |
|
|
console.log(` - 总API调用: ${apiMappings.reduce((sum, module) => sum + module.apiMappings.length, 0)} 个`) |
|
|
console.log(` - 总组件关联: ${apiMappings.reduce((sum, module) => |
|
|
sum + module.apiMappings.reduce((apiSum, api) => apiSum + (api.callingComponents ? api.callingComponents.length : 0), 0), 0)} 个`) |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 提取组件名称 |
|
|
extractComponentName(node) { |
|
|
if (node.type === 'Identifier') { |
|
|
return node.name |
|
|
} else if (node.type === 'StringLiteral') { |
|
|
return node.value |
|
|
} |
|
|
return null |
|
|
}, |
|
|
|
|
|
|
|
|
// 从路径提取模块名 |
|
|
extractModuleFromPath(path) { |
|
|
if (path === '/' || path === '') return 'home' |
|
|
|
|
|
const segments = path.split('/').filter(Boolean) |
|
|
if (segments.length === 0) return 'home' |
|
|
|
|
|
// 特殊处理:user-profile路径对应user-management模块 |
|
|
if (segments[0] === 'user-profile') { |
|
|
return 'user-management' |
|
|
} |
|
|
|
|
|
return segments[0] |
|
|
}, |
|
|
|
|
|
// 获取路由描述 |
|
|
getRouteDescription(module, path) { |
|
|
try { |
|
|
// 读取RouteConfig文件 |
|
|
const routeConfigPath = resolve(__dirname, '../src/renderer/modules/route-sync/RouteConfig.js') |
|
|
if (!existsSync(routeConfigPath)) { |
|
|
console.warn(`⚠️ RouteConfig文件不存在: ${routeConfigPath}`) |
|
|
return `${module}页面` |
|
|
} |
|
|
|
|
|
const routeConfigContent = readFileSync(routeConfigPath, 'utf-8') |
|
|
|
|
|
// 解析RouteConfig文件,提取descriptions对象 |
|
|
const descriptions = this.extractDescriptionsFromRouteConfig(routeConfigContent) |
|
|
|
|
|
// 查找对应的描述 |
|
|
if (descriptions && descriptions[module]) { |
|
|
return descriptions[module] |
|
|
} else { |
|
|
// 特殊处理:如果是user-profile路径,使用user-profile的描述 |
|
|
if (path === '/user-profile' && descriptions && descriptions['user-profile']) { |
|
|
return descriptions['user-profile'] |
|
|
} |
|
|
|
|
|
// 如果找不到对应的key,输出错误日志但不影响运行 |
|
|
console.error(`❌ RouteConfig.descriptions中缺少模块 "${module}" 的配置,路径: ${path}`) |
|
|
return `${module}页面` |
|
|
} |
|
|
} catch (error) { |
|
|
console.warn(`⚠️ 获取路由描述失败: ${error.message}`) |
|
|
return `${module}页面` |
|
|
} |
|
|
}, |
|
|
|
|
|
// 从RouteConfig文件中提取descriptions对象 |
|
|
extractDescriptionsFromRouteConfig(content) { |
|
|
try { |
|
|
const ast = parser.parse(content, { |
|
|
sourceType: 'module', |
|
|
plugins: ['jsx'] |
|
|
}) |
|
|
|
|
|
let descriptions = null |
|
|
|
|
|
traverse(ast, { |
|
|
ObjectExpression(path) { |
|
|
// 查找descriptions属性 |
|
|
const properties = path.node.properties |
|
|
const descriptionsProp = properties.find(prop => |
|
|
prop.key && prop.key.name === 'descriptions' |
|
|
) |
|
|
|
|
|
if (descriptionsProp && descriptionsProp.value && descriptionsProp.value.type === 'ObjectExpression') { |
|
|
// 解析descriptions对象 |
|
|
descriptions = {} |
|
|
descriptionsProp.value.properties.forEach(prop => { |
|
|
if (prop.key && prop.key.type === 'StringLiteral' && prop.value && prop.value.type === 'StringLiteral') { |
|
|
descriptions[prop.key.value] = prop.value.value |
|
|
} |
|
|
}) |
|
|
} |
|
|
} |
|
|
}) |
|
|
|
|
|
return descriptions |
|
|
} catch (error) { |
|
|
console.warn(`⚠️ 解析RouteConfig失败: ${error.message}`) |
|
|
return null |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports = { routeMappingPlugin }
|
|
|
|