|
|
|
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基础URL
|
|
|
|
* @returns {string} API基础URL
|
|
|
|
*/
|
|
|
|
getApiBaseUrl() {
|
|
|
|
try {
|
|
|
|
// 读取配置文件
|
|
|
|
const configPath = resolve(__dirname, '../../src/config/app.config.js')
|
|
|
|
if (existsSync(configPath)) {
|
|
|
|
const configContent = readFileSync(configPath, 'utf-8')
|
|
|
|
|
|
|
|
// 解析配置文件,提取apiBaseUrl
|
|
|
|
// 匹配 development 环境中的 apiBaseUrl 配置
|
|
|
|
const developmentMatch = configContent.match(/development:\s*\{[^}]*apiBaseUrl:\s*[^:]*:\s*['"`]([^'"`]+)['"`]/s)
|
|
|
|
if (developmentMatch) {
|
|
|
|
return developmentMatch[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果上面没匹配到,尝试匹配简单的字符串格式
|
|
|
|
const simpleMatch = configContent.match(/apiBaseUrl:\s*['"`]([^'"`]+)['"`]/)
|
|
|
|
if (simpleMatch) {
|
|
|
|
return simpleMatch[1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果无法从配置文件获取,返回空字符串(使用相对路径)
|
|
|
|
return ''
|
|
|
|
} catch (error) {
|
|
|
|
return ''
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 收集所有模块的API映射
|
|
|
|
* @param {Array} moduleDirs - 模块目录列表
|
|
|
|
* @returns {Array} API映射数组
|
|
|
|
*/
|
|
|
|
collectApiMappings(moduleDirs) {
|
|
|
|
this.apiMappings = []
|
|
|
|
|
|
|
|
moduleDirs.forEach(moduleName => {
|
|
|
|
const moduleApiMappings = this.collectModuleApiMappings(moduleName)
|
|
|
|
|
|
|
|
if (moduleApiMappings.length > 0) {
|
|
|
|
// 将 module 下沉到每个 API 映射中(serviceName已经在extractApiMapping中添加了)
|
|
|
|
moduleApiMappings.forEach(apiMapping => {
|
|
|
|
this.apiMappings.push({
|
|
|
|
...apiMapping,
|
|
|
|
module: moduleName
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
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 = []
|
|
|
|
|
|
|
|
let firstServiceName = null
|
|
|
|
|
|
|
|
serviceFiles.forEach(serviceFile => {
|
|
|
|
const servicePath = resolve(servicesPath, serviceFile)
|
|
|
|
const serviceName = serviceFile.replace('.js', '')
|
|
|
|
|
|
|
|
// 记录第一个服务名称(第一层需要)
|
|
|
|
if (!firstServiceName) {
|
|
|
|
firstServiceName = serviceName
|
|
|
|
}
|
|
|
|
|
|
|
|
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']
|
|
|
|
})
|
|
|
|
|
|
|
|
const self = this
|
|
|
|
traverse(ast, {
|
|
|
|
ObjectMethod(path) {
|
|
|
|
if (path.node.key && path.node.key.type === 'Identifier') {
|
|
|
|
const methodName = path.node.key.name
|
|
|
|
const apiMapping = self.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 = self.extractApiMapping(path.get('init'), methodName, serviceName, servicePath)
|
|
|
|
if (apiMapping) {
|
|
|
|
apiMappings.push(apiMapping)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} catch (error) {
|
|
|
|
}
|
|
|
|
|
|
|
|
return apiMappings
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 从AST节点中提取API映射信息 - 优化版本:只使用@serverApiMethod注解
|
|
|
|
* @param {Object} methodPath - 方法路径
|
|
|
|
* @param {string} methodName - 方法名称
|
|
|
|
* @param {string} serviceName - 服务名称
|
|
|
|
* @param {string} servicePath - 服务文件路径
|
|
|
|
* @returns {Object|null} API映射对象
|
|
|
|
*/
|
|
|
|
extractApiMapping(methodPath, methodName, serviceName, servicePath) {
|
|
|
|
// 只从@serverApiMethod注解中提取信息(强规则)
|
|
|
|
const annotationInfo = this.extractFromAnnotation(methodPath, servicePath)
|
|
|
|
if (annotationInfo) {
|
|
|
|
// 如果method是GET,跳过收集
|
|
|
|
if (annotationInfo.method === 'GET') {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
// 获取API基础URL
|
|
|
|
const apiBaseUrl = this.getApiBaseUrl()
|
|
|
|
|
|
|
|
// 组合完整的API路径
|
|
|
|
const fullPath = `${apiBaseUrl}${annotationInfo.path}`
|
|
|
|
|
|
|
|
// 提取路径部分,去掉协议、服务器地址和端口
|
|
|
|
const pathOnly = this.extractPathFromUrl(fullPath)
|
|
|
|
|
|
|
|
return {
|
|
|
|
apiMethodName: methodName,
|
|
|
|
method: annotationInfo.method,
|
|
|
|
path: pathOnly,
|
|
|
|
serviceName: serviceName
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果没有注解,返回null(不再回退到代码分析)
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 从完整URL中提取路径部分
|
|
|
|
* @param {string} fullUrl - 完整的URL
|
|
|
|
* @returns {string} 路径部分
|
|
|
|
*/
|
|
|
|
extractPathFromUrl(fullUrl) {
|
|
|
|
try {
|
|
|
|
// 如果URL包含协议,使用URL对象解析
|
|
|
|
if (fullUrl.includes('://')) {
|
|
|
|
const url = new URL(fullUrl)
|
|
|
|
// 解码路径,确保路径参数保持原始格式
|
|
|
|
return decodeURIComponent(url.pathname)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果没有协议,直接返回(已经是路径格式)
|
|
|
|
return fullUrl
|
|
|
|
} catch (error) {
|
|
|
|
// 如果解析失败,返回原始字符串
|
|
|
|
return fullUrl
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 从@serverApiMethod注解中提取API信息
|
|
|
|
* @param {Object} methodPath - 方法路径
|
|
|
|
* @param {string} servicePath - 服务文件路径
|
|
|
|
* @returns {Object|null} 注解信息
|
|
|
|
*/
|
|
|
|
extractFromAnnotation(methodPath, servicePath) {
|
|
|
|
try {
|
|
|
|
const content = readFileSync(servicePath, 'utf-8')
|
|
|
|
const lines = content.split('\n')
|
|
|
|
const methodLine = methodPath.node.loc ? methodPath.node.loc.start.line : 0
|
|
|
|
|
|
|
|
// 查找方法定义前的注释行(从方法行开始向上搜索)
|
|
|
|
for (let i = methodLine - 2; i >= 0; i--) {
|
|
|
|
const line = lines[i].trim()
|
|
|
|
|
|
|
|
// 查找@serverApiMethod注解
|
|
|
|
const annotationMatch = line.match(/\/\/\s*@serverApiMethod\s+(\w+)\s+(.+)/)
|
|
|
|
if (annotationMatch) {
|
|
|
|
const method = annotationMatch[1].toUpperCase()
|
|
|
|
const path = annotationMatch[2].trim()
|
|
|
|
|
|
|
|
return {
|
|
|
|
method: method,
|
|
|
|
path: path
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 如果遇到非注释行且不是空行,停止搜索
|
|
|
|
if (line && !line.startsWith('//') && !line.startsWith('*') && !line.startsWith('/*')) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
}
|
|
|
|
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = ApiCollector
|