diff --git a/frontend-source/scripts/extract-api-calls.js b/frontend-source/scripts/extract-api-calls.js new file mode 100644 index 00000000..4901e2af --- /dev/null +++ b/frontend-source/scripts/extract-api-calls.js @@ -0,0 +1,124 @@ +#!/usr/bin/env node +/** + * API 调用提取器 + * 从格式化后的 JS 中提取 HTTP 请求模式,输出 API 清单 JSON + * + * 用法: node extract-api-calls.js + */ + +const fs = require('fs'); +const path = require('path'); +const parser = require('@babel/parser'); +const traverse = require('@babel/traverse').default; + +function extractApiCalls(inputDir, outputFile) { + const apiCalls = []; + const files = fs.readdirSync(inputDir).filter(f => f.endsWith('.formatted.js')); + + for (const file of files) { + const code = fs.readFileSync(path.join(inputDir, file), 'utf-8'); + + try { + const ast = parser.parse(code, { + sourceType: 'script', + plugins: ['jsx', 'typescript', 'classProperties', 'dynamicImport'], + errorRecovery: true, + }); + + traverse(ast, { + // Match: axios.get('/api/xxx'), axios.post('/api/xxx', data) + CallExpression(nodePath) { + const node = nodePath.node; + const callee = node.callee; + + if ( + callee.type === 'MemberExpression' && + callee.object.type === 'Identifier' && + (callee.object.name === 'axios' || callee.object.name === 'http') + ) { + const method = callee.property.name; + if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) { + const url = node.arguments[0]; + if (url && url.type === 'StringLiteral') { + apiCalls.push({ + file: file, + method: method.toUpperCase(), + url: url.value, + hasBody: method !== 'get' && node.arguments.length > 1, + }); + } + } + } + + // this.$http.get|post + if ( + callee.type === 'MemberExpression' && + callee.object.type === 'MemberExpression' && + callee.object.property.name === '$http' + ) { + const method = callee.property.name; + const url = node.arguments[0]; + if (url && url.type === 'StringLiteral') { + apiCalls.push({ + file: file, + method: method.toUpperCase(), + url: url.value, + via: '$http', + }); + } + } + }, + + // Match string constants containing API paths + StringLiteral(nodePath) { + const value = nodePath.node.value; + if (typeof value === 'string' && value.startsWith('/api/') && value.length > 5) { + const parent = nodePath.parent; + if (parent.type === 'CallExpression') return; + + apiCalls.push({ + file: file, + method: 'REFERENCE', + url: value, + }); + } + }, + }); + } catch (e) { + console.error(`解析失败: ${file} — ${e.message}`); + } + } + + // Deduplicate + sort + const unique = {}; + apiCalls.forEach(call => { + const key = `${call.method} ${call.url}`; + if (!unique[key]) { + unique[key] = call; + } + }); + + const result = Object.values(unique).sort((a, b) => a.url.localeCompare(b.url)); + + fs.writeFileSync(outputFile, JSON.stringify(result, null, 2)); + console.log(`提取完成: ${result.length} 个 API 端点 → ${outputFile}`); + + // Also generate Markdown report + const mdPath = outputFile.replace('.json', '.md'); + let md = '# API 端点清单\n\n'; + md += `| Method | URL | 来源文件 |\n`; + md += `|--------|-----|----------|\n`; + result.forEach(call => { + md += '| ' + call.method + ' | `' + call.url + '` | ' + call.file + ' |\n'; + }); + fs.writeFileSync(mdPath, md); + console.log(`Markdown 清单: ${mdPath}`); +} + +const args = process.argv.slice(2); +if (args.length < 2) { + console.log('用法: node extract-api-calls.js '); + process.exit(1); +} + +extractApiCalls(args[0], args[1]);