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.

173 lines
5.1 KiB

3 days ago
const { readFileSync, existsSync, readdirSync } = require('fs')
const { resolve } = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
/**
* API收集模块
* 负责从service文件中收集API信息
*/
class ApiCollector {
constructor() {
this.apiMappings = []
}
/**
* 收集所有模块的API映射
* @param {Array} moduleDirs - 模块目录列表
* @returns {Array} API映射数组
*/
collectApiMappings(moduleDirs) {
this.apiMappings = []
moduleDirs.forEach(moduleName => {
const moduleApiMappings = this.collectModuleApiMappings(moduleName)
if (moduleApiMappings.length > 0) {
this.apiMappings.push({
module: moduleName,
serviceName: moduleApiMappings[0].serviceName,
servicePath: moduleApiMappings[0].servicePath,
apiMappings: moduleApiMappings
})
}
})
return this.apiMappings
}
/**
* 收集单个模块的API映射
* @param {string} moduleName - 模块名称
* @returns {Array} 模块的API映射数组
*/
collectModuleApiMappings(moduleName) {
const servicesPath = resolve(__dirname, '../../src/renderer/modules', moduleName, 'services')
if (!existsSync(servicesPath)) {
return []
}
const serviceFiles = readdirSync(servicesPath).filter(file => file.endsWith('.js'))
const moduleApiMappings = []
serviceFiles.forEach(serviceFile => {
const servicePath = resolve(servicesPath, serviceFile)
const serviceName = serviceFile.replace('.js', '')
try {
const serviceApiMappings = this.analyzeServiceFile(servicePath, serviceName, moduleName)
moduleApiMappings.push(...serviceApiMappings)
} catch (error) {
// 静默处理错误
}
})
return moduleApiMappings
}
/**
* 分析单个服务文件的API映射
* @param {string} servicePath - 服务文件路径
* @param {string} serviceName - 服务名称
* @param {string} moduleName - 模块名称
* @returns {Array} API映射数组
*/
analyzeServiceFile(servicePath, serviceName, moduleName) {
const content = readFileSync(servicePath, 'utf-8')
const apiMappings = []
try {
const ast = parser.parse(content, {
sourceType: 'module',
plugins: ['jsx', 'typescript']
})
traverse(ast, {
ObjectMethod(path) {
if (path.node.key && path.node.key.type === 'Identifier') {
const methodName = path.node.key.name
const apiMapping = this.extractApiMapping(path, methodName, serviceName, servicePath)
if (apiMapping) {
apiMappings.push(apiMapping)
}
}
},
VariableDeclarator(path) {
if (path.node.id && path.node.id.type === 'Identifier' &&
path.node.init && path.node.init.type === 'ArrowFunctionExpression') {
const methodName = path.node.id.name
const apiMapping = this.extractApiMapping(path.get('init'), methodName, serviceName, servicePath)
if (apiMapping) {
apiMappings.push(apiMapping)
}
}
}
})
} catch (error) {
// 静默处理错误
}
return apiMappings
}
/**
* 从AST节点中提取API映射信息
* @param {Object} methodPath - 方法路径
* @param {string} methodName - 方法名称
* @param {string} serviceName - 服务名称
* @param {string} servicePath - 服务文件路径
* @returns {Object|null} API映射对象
*/
extractApiMapping(methodPath, methodName, serviceName, servicePath) {
let httpMethod = null
let apiPath = null
// 查找HTTP方法调用
traverse(methodPath.node, {
CallExpression(path) {
const node = path.node
if (node.callee && node.callee.type === 'MemberExpression') {
const object = node.callee.object
const property = node.callee.property
if (object && property &&
object.type === 'Identifier' &&
property.type === 'Identifier') {
const method = property.name.toLowerCase()
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) {
httpMethod = method.toUpperCase()
// 提取API路径
if (node.arguments && node.arguments.length > 0) {
const firstArg = node.arguments[0]
if (firstArg.type === 'StringLiteral') {
apiPath = firstArg.value
} else if (firstArg.type === 'TemplateLiteral') {
apiPath = firstArg.quasis[0].value.raw
}
}
}
}
}
}
}, methodPath.scope, methodPath)
if (httpMethod && apiPath) {
return {
apiMethodName: methodName,
method: httpMethod,
path: apiPath,
line: methodPath.node.loc ? methodPath.node.loc.start.line : 0,
serviceName: serviceName,
servicePath: servicePath
}
}
return null
}
}
module.exports = ApiCollector