You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
461 lines
14 KiB
461 lines
14 KiB
24 hours ago
|
const { readFileSync, existsSync, readdirSync } = require('fs')
|
||
|
const { resolve } = require('path')
|
||
|
const parser = require('@babel/parser')
|
||
|
const traverse = require('@babel/traverse').default
|
||
|
const { parse } = require('@vue/compiler-dom')
|
||
|
|
||
|
/**
|
||
|
* 基于AST的组件关系分析器
|
||
|
* 使用vue-template-compiler解析模板,babel解析JS部分
|
||
|
*/
|
||
|
class ComponentRelationshipAnalyzerAST {
|
||
|
constructor() {
|
||
|
this.componentRelationships = []
|
||
|
this.processedComponents = new Set()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 分析组件关系
|
||
|
* @param {Array} routes - 路由数组
|
||
|
* @param {Array} apiMappings - API映射数组
|
||
|
* @returns {Array} 组件关系列表
|
||
|
*/
|
||
|
analyzeComponentRelationships(routes, apiMappings) {
|
||
|
this.componentRelationships = []
|
||
|
this.processedComponents.clear()
|
||
|
|
||
|
// 获取所有顶级组件(routes中的组件)并分配ID
|
||
|
const topLevelComponents = routes.map((route, index) => ({
|
||
|
id: index + 1, // 为routes分配ID
|
||
|
component: route.component,
|
||
|
module: this.getModuleFromRoute(route),
|
||
|
path: route.path
|
||
|
}))
|
||
|
|
||
|
// 获取所有触发源组件并分配ID
|
||
|
let triggerSourceId = 1
|
||
|
const triggerSourceComponents = this.extractTriggerSourceComponents(apiMappings).map(component => ({
|
||
|
id: triggerSourceId++, // 为triggerSource分配ID
|
||
|
...component
|
||
|
}))
|
||
|
|
||
|
|
||
|
// 为每个触发源组件分析其与顶级组件的关系
|
||
|
triggerSourceComponents.forEach(triggerComponent => {
|
||
|
this.analyzeComponentToTopLevel(triggerComponent, topLevelComponents)
|
||
|
})
|
||
|
|
||
|
return this.componentRelationships
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 从路由中获取模块信息
|
||
|
* @param {Object} route - 路由对象
|
||
|
* @returns {string} 模块名称
|
||
|
*/
|
||
|
getModuleFromRoute(route) {
|
||
|
// 直接使用路由中的module字段,不再进行推断
|
||
|
return route.module || 'unknown'
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 提取所有触发源组件
|
||
|
* @param {Array} apiMappings - API映射数组
|
||
|
* @returns {Array} 触发源组件数组
|
||
|
*/
|
||
|
extractTriggerSourceComponents(apiMappings) {
|
||
|
const components = []
|
||
|
|
||
|
apiMappings.forEach(apiMapping => {
|
||
|
if (apiMapping.triggerSources) {
|
||
|
apiMapping.triggerSources.forEach(triggerSource => {
|
||
|
components.push({
|
||
|
component: triggerSource.component,
|
||
|
module: triggerSource.module,
|
||
|
triggerName: triggerSource.triggerName,
|
||
|
triggerType: triggerSource.triggerType,
|
||
|
apiMethodName: apiMapping.apiMethodName,
|
||
|
apiModule: apiMapping.module
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return components
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 分析组件到顶级组件的关系
|
||
|
* @param {Object} triggerComponent - 触发源组件
|
||
|
* @param {Array} topLevelComponents - 顶级组件数组
|
||
|
*/
|
||
|
analyzeComponentToTopLevel(triggerComponent, topLevelComponents) {
|
||
|
const triggerKey = `${triggerComponent.id}:${triggerComponent.module}:${triggerComponent.component}`
|
||
|
|
||
|
// 避免重复处理同一个触发源
|
||
|
if (this.processedComponents.has(triggerKey)) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
this.processedComponents.add(triggerKey)
|
||
|
|
||
|
// 如果触发源组件本身就是顶级组件,直接建立关系
|
||
|
const isTopLevel = topLevelComponents.find(top =>
|
||
|
top.component === triggerComponent.component && top.module === triggerComponent.module
|
||
|
)
|
||
|
|
||
|
if (isTopLevel) {
|
||
|
this.componentRelationships.push({
|
||
|
triggerSourceId: triggerComponent.id, // 使用ID引用
|
||
|
routeId: isTopLevel.id, // 使用ID引用
|
||
|
relationshipType: 'self'
|
||
|
})
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// 查找父组件关系
|
||
|
const parentComponents = this.findParentComponentsAST(triggerComponent)
|
||
|
let hasRelationship = false
|
||
|
|
||
|
|
||
|
if (parentComponents.length > 0) {
|
||
|
parentComponents.forEach(parent => {
|
||
|
// 递归查找父组件的父组件,直到找到顶级组件
|
||
|
const found = this.findTopLevelAncestors(parent, triggerComponent, topLevelComponents)
|
||
|
if (found) hasRelationship = true
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// 如果没有找到父组件关系,尝试直接匹配顶级组件
|
||
|
if (!hasRelationship) {
|
||
|
const directMatch = topLevelComponents.find(top =>
|
||
|
top.component === triggerComponent.component ||
|
||
|
top.module === triggerComponent.module
|
||
|
)
|
||
|
|
||
|
if (directMatch) {
|
||
|
this.componentRelationships.push({
|
||
|
triggerSourceId: triggerComponent.id,
|
||
|
routeId: directMatch.id,
|
||
|
relationshipType: 'direct'
|
||
|
})
|
||
|
hasRelationship = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 最后的兜底:确保每个触发源都至少有一个关系记录
|
||
|
if (!hasRelationship) {
|
||
|
this.componentRelationships.push({
|
||
|
triggerSourceId: triggerComponent.id,
|
||
|
routeId: 1, // 默认关联到第一个路由
|
||
|
relationshipType: 'fallback'
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 使用AST解析查找组件的父组件
|
||
|
* @param {Object} component - 组件信息
|
||
|
* @returns {Array} 父组件数组
|
||
|
*/
|
||
|
findParentComponentsAST(component) {
|
||
|
const parents = []
|
||
|
|
||
|
try {
|
||
|
// 查找所有模块中的组件,看哪些引用了当前组件
|
||
|
const modulesPath = resolve(__dirname, '../../src/renderer/modules')
|
||
|
if (existsSync(modulesPath)) {
|
||
|
const moduleDirs = readdirSync(modulesPath, { withFileTypes: true })
|
||
|
.filter(dirent => dirent.isDirectory())
|
||
|
.map(dirent => dirent.name)
|
||
|
|
||
|
moduleDirs.forEach(moduleName => {
|
||
|
const parentComponents = this.findComponentsInModuleAST(moduleName, component)
|
||
|
parents.push(...parentComponents)
|
||
|
})
|
||
|
}
|
||
|
} catch (error) {
|
||
|
// 静默处理错误
|
||
|
}
|
||
|
|
||
|
return parents
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 在指定模块中使用AST解析查找引用目标组件的组件
|
||
|
* @param {string} moduleName - 模块名称
|
||
|
* @param {Object} targetComponent - 目标组件
|
||
|
* @returns {Array} 父组件数组
|
||
|
*/
|
||
|
findComponentsInModuleAST(moduleName, targetComponent) {
|
||
|
const parents = []
|
||
|
|
||
|
try {
|
||
|
const modulePath = resolve(__dirname, '../../src/renderer/modules', moduleName)
|
||
|
if (!existsSync(modulePath)) return parents
|
||
|
|
||
|
// 查找views目录
|
||
|
const viewsPath = resolve(modulePath, 'views')
|
||
|
if (existsSync(viewsPath)) {
|
||
|
const viewFiles = readdirSync(viewsPath).filter(file => file.endsWith('.vue'))
|
||
|
viewFiles.forEach(file => {
|
||
|
const componentName = file.replace('.vue', '')
|
||
|
if (this.componentReferencesTargetAST(viewsPath, file, targetComponent)) {
|
||
|
parents.push({
|
||
|
component: componentName,
|
||
|
module: moduleName,
|
||
|
type: 'view'
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// 查找components目录
|
||
|
const componentsPath = resolve(modulePath, 'components')
|
||
|
if (existsSync(componentsPath)) {
|
||
|
const componentFiles = readdirSync(componentsPath).filter(file => file.endsWith('.vue'))
|
||
|
componentFiles.forEach(file => {
|
||
|
const componentName = file.replace('.vue', '')
|
||
|
if (this.componentReferencesTargetAST(componentsPath, file, targetComponent)) {
|
||
|
parents.push({
|
||
|
component: componentName,
|
||
|
module: moduleName,
|
||
|
type: 'component'
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
} catch (error) {
|
||
|
// 静默处理错误
|
||
|
}
|
||
|
|
||
|
return parents
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 使用AST解析检查组件是否引用了目标组件
|
||
|
* @param {string} dirPath - 目录路径
|
||
|
* @param {string} fileName - 文件名
|
||
|
* @param {Object} targetComponent - 目标组件
|
||
|
* @returns {boolean} 是否引用
|
||
|
*/
|
||
|
componentReferencesTargetAST(dirPath, fileName, targetComponent) {
|
||
|
try {
|
||
|
const filePath = resolve(dirPath, fileName)
|
||
|
const content = readFileSync(filePath, 'utf-8')
|
||
|
|
||
|
// 解析Vue单文件组件
|
||
|
const { script, template } = this.parseVueSFC(content)
|
||
|
|
||
|
// 检查script部分的import和components注册
|
||
|
if (script && this.analyzeScriptAST(script, targetComponent)) {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// 检查template部分的使用
|
||
|
if (template && this.analyzeTemplateAST(template, targetComponent)) {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
} catch (error) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 解析Vue单文件组件
|
||
|
* @param {string} content - 文件内容
|
||
|
* @returns {Object} 解析结果
|
||
|
*/
|
||
|
parseVueSFC(content) {
|
||
|
const result = { script: null, template: null }
|
||
|
|
||
|
try {
|
||
|
// 提取script部分
|
||
|
const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/)
|
||
|
if (scriptMatch) {
|
||
|
result.script = scriptMatch[1]
|
||
|
}
|
||
|
|
||
|
// 提取template部分
|
||
|
const templateMatch = content.match(/<template[^>]*>([\s\S]*?)<\/template>/)
|
||
|
if (templateMatch) {
|
||
|
result.template = templateMatch[1]
|
||
|
}
|
||
|
} catch (error) {
|
||
|
// 静默处理错误
|
||
|
}
|
||
|
|
||
|
return result
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 使用Babel AST分析script部分
|
||
|
* @param {string} scriptContent - script内容
|
||
|
* @param {Object} targetComponent - 目标组件
|
||
|
* @returns {boolean} 是否引用
|
||
|
*/
|
||
|
analyzeScriptAST(scriptContent, targetComponent) {
|
||
|
try {
|
||
|
const ast = parser.parse(scriptContent, {
|
||
|
sourceType: 'module',
|
||
|
plugins: ['jsx', 'typescript']
|
||
|
})
|
||
|
|
||
|
let hasReference = false
|
||
|
|
||
|
traverse(ast, {
|
||
|
// 检查import语句 - 更精确的匹配
|
||
|
ImportDeclaration(path) {
|
||
|
const source = path.node.source.value
|
||
|
// 检查import的默认导入
|
||
|
if (path.node.specifiers) {
|
||
|
path.node.specifiers.forEach(spec => {
|
||
|
if (spec.type === 'ImportDefaultSpecifier' &&
|
||
|
spec.local.name === targetComponent.component) {
|
||
|
hasReference = true
|
||
|
}
|
||
|
// 检查命名导入
|
||
|
if (spec.type === 'ImportSpecifier' &&
|
||
|
spec.imported.name === targetComponent.component) {
|
||
|
hasReference = true
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// 检查components注册 - 支持多种格式
|
||
|
ObjectProperty(path) {
|
||
|
if (path.node.key.name === 'components') {
|
||
|
const value = path.node.value
|
||
|
if (value.type === 'ObjectExpression') {
|
||
|
value.properties.forEach(prop => {
|
||
|
// 支持 components: { ComponentName: ComponentName }
|
||
|
if (prop.key.name === targetComponent.component) {
|
||
|
hasReference = true
|
||
|
}
|
||
|
// 支持 components: { ComponentName }
|
||
|
if (prop.type === 'ObjectProperty' &&
|
||
|
prop.key.name === targetComponent.component) {
|
||
|
hasReference = true
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
|
||
|
// 检查Vue 3 Composition API的组件注册
|
||
|
CallExpression(path) {
|
||
|
if (path.node.callee.name === 'defineComponent') {
|
||
|
const arg = path.node.arguments[0]
|
||
|
if (arg && arg.type === 'ObjectExpression') {
|
||
|
arg.properties.forEach(prop => {
|
||
|
if (prop.key.name === 'components') {
|
||
|
const componentsValue = prop.value
|
||
|
if (componentsValue.type === 'ObjectExpression') {
|
||
|
componentsValue.properties.forEach(compProp => {
|
||
|
if (compProp.key.name === targetComponent.component) {
|
||
|
hasReference = true
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
|
||
|
return hasReference
|
||
|
} catch (error) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 使用Vue template compiler分析template部分
|
||
|
* @param {string} templateContent - template内容
|
||
|
* @param {Object} targetComponent - 目标组件
|
||
|
* @returns {boolean} 是否引用
|
||
|
*/
|
||
|
analyzeTemplateAST(templateContent, targetComponent) {
|
||
|
try {
|
||
|
const ast = parse(templateContent)
|
||
|
|
||
|
let hasReference = false
|
||
|
|
||
|
// 遍历AST查找组件使用
|
||
|
const traverse = (node) => {
|
||
|
if (node.type === 1) { // Element
|
||
|
// 检查标签名是否匹配目标组件
|
||
|
if (node.tag === targetComponent.component) {
|
||
|
hasReference = true
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// 检查动态组件
|
||
|
if (node.tag === 'component' && node.props) {
|
||
|
node.props.forEach(prop => {
|
||
|
if (prop.name === 'is' && prop.value &&
|
||
|
prop.value.content === targetComponent.component) {
|
||
|
hasReference = true
|
||
|
return
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// 递归遍历子节点
|
||
|
if (node.children) {
|
||
|
node.children.forEach(child => {
|
||
|
if (child.type === 1) { // Element
|
||
|
traverse(child)
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
traverse(ast)
|
||
|
return hasReference
|
||
|
} catch (error) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 查找顶级祖先组件
|
||
|
* @param {Object} parentComponent - 父组件
|
||
|
* @param {Object} originalTrigger - 原始触发组件
|
||
|
* @param {Array} topLevelComponents - 顶级组件数组
|
||
|
* @returns {boolean} 是否找到了关系
|
||
|
*/
|
||
|
findTopLevelAncestors(parentComponent, originalTrigger, topLevelComponents) {
|
||
|
// 检查父组件是否是顶级组件
|
||
|
const isTopLevel = topLevelComponents.find(top =>
|
||
|
top.component === parentComponent.component && top.module === parentComponent.module
|
||
|
)
|
||
|
|
||
|
if (isTopLevel) {
|
||
|
// 找到顶级组件,建立关系
|
||
|
this.componentRelationships.push({
|
||
|
triggerSourceId: originalTrigger.id, // 使用ID引用
|
||
|
routeId: isTopLevel.id, // 使用ID引用
|
||
|
relationshipType: 'ancestor'
|
||
|
})
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// 继续向上查找
|
||
|
const grandParents = this.findParentComponentsAST(parentComponent)
|
||
|
let found = false
|
||
|
grandParents.forEach(grandParent => {
|
||
|
const result = this.findTopLevelAncestors(grandParent, originalTrigger, topLevelComponents)
|
||
|
if (result) found = true
|
||
|
})
|
||
|
return found
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = ComponentRelationshipAnalyzerAST
|