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.
 
 
 
 
 
 

702 lines
22 KiB

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. 分析页面组件(第一层)
const pageMappings = this.analyzePageComponents(routes)
// 3. 分析弹窗组件(第二层)
const modalMappings = this.analyzeModalComponents()
// 4. 生成映射文件
this.generateMappingFile(pageMappings, modalMappings)
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)
routes.push(route)
}
}
})
return routes
},
// 分析页面组件(第一层)
analyzePageComponents(routes) {
const pageMappings = []
routes.forEach(route => {
if (route.component) {
const componentAnalysis = this.analyzeComponent(route.component, route.path)
if (componentAnalysis && componentAnalysis.apiCalls.length > 0) {
pageMappings.push({
route: route.path,
routeName: route.name,
component: route.component,
module: route.module,
layer: 'page',
apiCalls: componentAnalysis.apiCalls,
methods: componentAnalysis.methods
})
}
}
})
return pageMappings
},
// 分析弹窗组件(第二层)
analyzeModalComponents() {
const modalMappings = []
const modalFiles = this.findModalFiles()
modalFiles.forEach(modalFile => {
const analysis = this.analyzeComponent(modalFile.name, modalFile.path)
if (analysis && analysis.apiCalls.length > 0) {
modalMappings.push({
component: modalFile.name,
path: modalFile.path,
module: modalFile.module,
layer: 'modal',
apiCalls: analysis.apiCalls,
methods: analysis.methods
})
}
})
return modalMappings
},
// 分析单个组件
analyzeComponent(componentName, componentPath) {
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.analyzeVueComponent(content, componentName)
} else {
return this.analyzeJavaScriptComponent(content, componentName)
}
} catch (error) {
console.warn(` 分析组件失败: ${componentName}`, error.message)
return null
}
},
// 分析Vue组件
analyzeVueComponent(content, componentName) {
const { descriptor } = parse(content)
const scriptContent = descriptor.script?.content || ''
if (!scriptContent) {
return { apiCalls: [], methods: [] }
}
const ast = parser.parse(scriptContent, {
sourceType: 'module',
plugins: ['jsx']
})
return this.extractApiCallsFromAST(ast, componentName)
},
// 分析JavaScript组件
analyzeJavaScriptComponent(content, componentName) {
const ast = parser.parse(content, {
sourceType: 'module',
plugins: ['jsx']
})
return this.extractApiCallsFromAST(ast, componentName)
},
// 从AST中提取API调用
extractApiCallsFromAST(ast, componentName) {
const apiCalls = []
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)
}
},
// 查找API调用
CallExpression(path) {
const callInfo = self.extractApiCall(path)
if (callInfo) {
apiCalls.push(callInfo)
}
}
})
return { apiCalls, methods }
},
// 提取API调用信息
extractApiCall(path) {
const node = path.node
const self = this
// 检查是否是服务调用 (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 => self.extractArgumentValue(arg)),
line: node.loc?.start?.line || 0
}
}
}
}
// 检查是否是直接的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) {
if (object.name === 'api' || object.name === 'axios' || object.name === 'http') {
const method = property.name
const args = node.arguments
if (args.length > 0 && args[0].type === 'StringLiteral') {
return {
type: 'api',
method: method.toUpperCase(),
path: args[0].value,
arguments: args.slice(1).map(arg => self.extractArgumentValue(arg)),
line: node.loc?.start?.line || 0
}
}
}
}
}
return null
},
// 从模块配置中读取模块名称
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 []
}
},
// 查找弹窗文件
findModalFiles() {
const modalFiles = []
const moduleDirs = this.readModuleNamesFromConfig()
moduleDirs.forEach(moduleName => {
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 => {
modalFiles.push({
name: file.replace('.vue', ''),
path: resolve(componentsPath, file),
module: moduleName
})
})
}
})
return modalFiles
},
// 查找组件文件
findComponentFile(componentName) {
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
},
// 生成映射文件
generateMappingFile(pageMappings, modalMappings) {
const outputPath = resolve(__dirname, '../src/renderer/modules/route-sync/direct-route-mappings.js')
// 检查文件内容是否真的需要更新
if (this._isFileContentSame(pageMappings, modalMappings, outputPath)) {
console.log('⏭ 路由映射文件内容未变化,跳过写入')
return
}
const mappingContent = `// 第一阶段:直接路由-API映射关系
// 此文件由 route-mapping-plugin 在构建时生成
// 收集页面和弹窗组件的直接API调用关系
// 请勿手动修改
export const directRouteMappings = {
// 第一层:页面组件的数据操作API
pageMappings: ${JSON.stringify(pageMappings, null, 2)},
// 第二层:弹窗组件的数据操作API
modalMappings: ${JSON.stringify(modalMappings, null, 2)},
// 分析信息
analysisInfo: {
phase: 'phase1',
description: '第一阶段:收集直接映射关系',
timestamp: new Date().toISOString(),
pageCount: ${pageMappings.length},
modalCount: ${modalMappings.length},
totalApiCalls: ${pageMappings.reduce((sum, p) => sum + p.apiCalls.length, 0) + modalMappings.reduce((sum, m) => sum + m.apiCalls.length, 0)}
}
}
// 按模块分组的映射
export const moduleMappings = ${JSON.stringify(this.groupMappingsByModule(pageMappings, modalMappings), null, 2)}
// 按API路径分组的映射
export const apiPathMappings = ${JSON.stringify(this.groupMappingsByApiPath(pageMappings, modalMappings), null, 2)}
export default directRouteMappings
`
fs.writeFileSync(outputPath, mappingContent, 'utf-8')
console.log(`✅ 直接映射关系文件已生成: ${outputPath}`)
console.log(`📊 统计信息:`)
console.log(` - 页面组件: ${pageMappings.length}`)
console.log(` - 弹窗组件: ${modalMappings.length}`)
console.log(` - 总API调用: ${pageMappings.reduce((sum, p) => sum + p.apiCalls.length, 0) + modalMappings.reduce((sum, m) => sum + m.apiCalls.length, 0)}`)
},
// 检查文件内容是否相同
_isFileContentSame(pageMappings, modalMappings, outputPath) {
if (!existsSync(outputPath)) {
return false
}
try {
const currentContent = fs.readFileSync(outputPath, 'utf-8')
// 提取当前文件中的映射数据
const currentDataMatch = currentContent.match(/export const directRouteMappings = ({[\s\S]*?});/)
if (!currentDataMatch) {
return false
}
// 解析当前数据
const currentData = JSON.parse(currentDataMatch[1])
// 比较数据
const newData = {
pageMappings,
modalMappings,
analysisInfo: {
phase: 'phase1',
description: '第一阶段:收集直接映射关系',
timestamp: new Date().toISOString(),
pageCount: pageMappings.length,
modalCount: modalMappings.length,
totalApiCalls: pageMappings.reduce((sum, p) => sum + p.apiCalls.length, 0) + modalMappings.reduce((sum, m) => sum + m.apiCalls.length, 0)
}
}
// 忽略时间戳差异进行比较
const currentDataForCompare = { ...currentData }
const newDataForCompare = { ...newData }
delete currentDataForCompare.analysisInfo.timestamp
delete newDataForCompare.analysisInfo.timestamp
return JSON.stringify(currentDataForCompare) === JSON.stringify(newDataForCompare)
} catch (error) {
console.warn('⚠ 比较文件内容时出错:', error.message)
return false
}
},
// 按模块分组映射
groupMappingsByModule(pageMappings, modalMappings) {
const grouped = {}
// 分组页面映射
pageMappings.forEach(mapping => {
const module = mapping.module || 'unknown'
if (!grouped[module]) {
grouped[module] = { pages: [], modals: [] }
}
grouped[module].pages.push(mapping)
})
// 分组弹窗映射
modalMappings.forEach(mapping => {
const module = mapping.module || 'unknown'
if (!grouped[module]) {
grouped[module] = { pages: [], modals: [] }
}
grouped[module].modals.push(mapping)
})
return grouped
},
// 按API路径分组映射
groupMappingsByApiPath(pageMappings, modalMappings) {
const grouped = {}
const addToGroup = (mapping, layer) => {
mapping.apiCalls.forEach(apiCall => {
let key = ''
if (apiCall.type === 'api') {
key = `${apiCall.method} ${apiCall.path}`
} else if (apiCall.type === 'service') {
key = `${apiCall.service}.${apiCall.method}`
}
if (key) {
if (!grouped[key]) {
grouped[key] = { pages: [], modals: [] }
}
grouped[key][layer].push({
component: mapping.component,
module: mapping.module,
route: mapping.route || mapping.path,
apiCall: apiCall
})
}
})
}
pageMappings.forEach(mapping => addToGroup(mapping, 'pages'))
modalMappings.forEach(mapping => addToGroup(mapping, 'modals'))
return grouped
},
// 提取组件名称
extractComponentName(node) {
if (node.type === 'Identifier') {
return node.name
} else if (node.type === 'StringLiteral') {
return node.value
}
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
},
// 从路径提取模块名
extractModuleFromPath(path) {
if (path === '/' || path === '') return 'home'
const segments = path.split('/').filter(Boolean)
if (segments.length === 0) return 'home'
return segments[0]
}
}
}
module.exports = { routeMappingPlugin }