#!/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]);