const fs = require('fs'); const path = require('path'); function listFiles(dir, exts) { const results = []; function walk(d) { let entries; try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; } for (const e of entries) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p); else if (exts.some(ext => p.endsWith(ext))) results.push(p); } } walk(dir); return results; } function readLines(fp) { try { return fs.readFileSync(fp, 'utf8').split(/\r?\n/); } catch { return []; } } function extractNestControllers(file) { const lines = readLines(file); const res = []; let classBasePath = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const m = line.match(/@Controller\(([^)]*)\)/); if (m) { const arg = (m[1] || '').trim(); const p = arg.replace(/["'`]/g, ''); classBasePath = p || ''; res.push({ type: 'controller', basePath: classBasePath, file, line: i + 1 }); } } const methods = []; const httpDecorators = ['Get', 'Post', 'Put', 'Delete', 'Patch', 'All', 'Options', 'Head']; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const m = line.match(/@([A-Za-z]+)\(([^)]*)\)/); if (m && httpDecorators.includes(m[1])) { const method = m[1].toUpperCase(); const arg = (m[2] || '').trim(); let subPath = ''; const sp = arg.match(/["'`]([^"'`]+)["'`]/); if (sp) subPath = sp[1]; // find method name in following lines let methodName = ''; for (let j = i + 1; j < Math.min(lines.length, i + 10); j++) { const s = lines[j].trim(); const mm = s.match(/(async\s+)?([A-Za-z0-9_]+)\s*\(/); if (mm) { methodName = mm[2]; break; } } methods.push({ httpMethod: method, subPath, file, line: i + 1, methodName }); } } return { controllers: res, methods }; } function extractJavaControllers(file) { const lines = readLines(file); const res = []; let basePath = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (/@RestController/.test(line) || /@Controller/.test(line)) { res.push({ type: 'controller', file, line: i + 1 }); } const m = line.match(/@RequestMapping\(([^)]*)\)/); if (m) { const arg = (m[1] || '').trim(); const sp = arg.match(/["'`]([^"'`]+)["'`]/); if (sp) basePath = sp[1]; res.push({ type: 'base', basePath, file, line: i + 1 }); } } const methods = []; const httpDecorators = [ { dec: 'GetMapping', m: 'GET' }, { dec: 'PostMapping', m: 'POST' }, { dec: 'PutMapping', m: 'PUT' }, { dec: 'DeleteMapping', m: 'DELETE' }, { dec: 'PatchMapping', m: 'PATCH' }, { dec: 'RequestMapping', m: 'MIXED' }, ]; for (let i = 0; i < lines.length; i++) { const line = lines[i]; for (const hd of httpDecorators) { const re = new RegExp('@' + hd.dec + '\(([^)]*)\)'); const m = line.match(re); if (m) { const arg = (m[1] || '').trim(); let subPath = ''; const sp = arg.match(/["'`]([^"'`]+)["'`]/); if (sp) subPath = sp[1]; // find method name let methodName = ''; for (let j = i + 1; j < Math.min(lines.length, i + 10); j++) { const s = lines[j].trim(); const mm = s.match(/public\s+[^\s]+\s+([A-Za-z0-9_]+)\s*\(/); if (mm) { methodName = mm[1]; break; } } methods.push({ httpMethod: hd.m, subPath, file, line: i + 1, methodName }); } } } // attach class-level basePath let classBase = ''; for (const r of res) { if (r.basePath) classBase = r.basePath; } return { controllers: res, methods, basePath: classBase }; } function rel(p) { const root = process.cwd(); return path.relative(root, p); } function groupPrefixNest(p) { const idx = p.indexOf('/controllers/'); if (idx >= 0) { const segs = p.slice(idx + '/controllers/'.length).split('/'); return segs[0] || ''; } return ''; } function buildReport() { const root = process.cwd(); const nestRoot = path.join(root, 'wwjcloud-nest-v1', 'wwjcloud'); const javaRoot = path.join(root, 'niucloud-java'); const nestFiles = listFiles(nestRoot, ['.ts']).filter(f => { const content = readLines(f).join('\n'); return content.includes('@Controller('); }); const javaFiles = listFiles(javaRoot, ['.java']).filter(f => { const content = readLines(f).join('\n'); return content.includes('@RestController') || content.includes('@Controller'); }); const nestRoutes = []; for (const f of nestFiles) { const parsed = extractNestControllers(f); const basePath = (parsed.controllers.find(c => c.basePath) || {}).basePath || ''; for (const m of parsed.methods) { nestRoutes.push({ side: 'nest', file: rel(f), line: m.line, basePath, subPath: m.subPath, httpMethod: m.httpMethod, methodName: m.methodName, prefix: groupPrefixNest(rel(f)), }); } } const javaRoutes = []; for (const f of javaFiles) { const parsed = extractJavaControllers(f); const basePath = parsed.basePath || ''; for (const m of parsed.methods) { javaRoutes.push({ side: 'java', file: rel(f), line: m.line, basePath, subPath: m.subPath, httpMethod: m.httpMethod, methodName: m.methodName, prefix: (() => { const m = rel(f).match(/controller\/(adminapi|api)\//); return m ? m[1] : ''; })(), }); } } function fullPath(r) { const bp = (r.basePath || '').replace(/\/$/, ''); const sp = (r.subPath || '').replace(/^\//, ''); let p = (bp ? bp + (sp ? '/' + sp : '') : (sp || '')) || ''; if (!p.startsWith('/')) p = '/' + p; return p; } const nestMap = new Map(); for (const r of nestRoutes) nestMap.set(r.httpMethod + ' ' + fullPath(r), r); const javaMap = new Map(); for (const r of javaRoutes) javaMap.set(r.httpMethod + ' ' + fullPath(r), r); const diffs = []; for (const [k, jr] of javaMap.entries()) { if (!nestMap.has(k)) diffs.push({ type: 'missing_in_nest', expected: k, java: jr }); } for (const [k, nr] of nestMap.entries()) { if (!javaMap.has(k)) diffs.push({ type: 'missing_in_java', expected: k, nest: nr }); } const summary = { counts: { nest: nestRoutes.length, java: javaRoutes.length, }, byPrefix: { nest: nestRoutes.reduce((acc, r) => { acc[r.prefix] = (acc[r.prefix] || 0) + 1; return acc; }, {}), java: javaRoutes.reduce((acc, r) => { acc[r.prefix] = (acc[r.prefix] || 0) + 1; return acc; }, {}), }, }; function moduleKeyNest(relFile) { const i = relFile.indexOf('controllers/'); if (i < 0) return ''; const parts = relFile.slice(i + 'controllers/'.length).split('/'); const pfx = parts[0] || ''; // 对于adminapi/shop/goods/shop-goods.controller.ts这样的路径 // 应该返回adminapi/goods而不是adminapi/shop if (parts.length >= 3) { const businessModule = parts[2]; // goods, order, marketing等 return pfx + '/' + businessModule; } const mod = parts[1] || ''; return (pfx && mod) ? (pfx + '/' + mod) : pfx; } function moduleKeyJava(relFile) { const m = relFile.match(/controller\/(adminapi|api)\/(.*?)\//); if (!m) return ''; const pfx = m[1]; const mod = m[2]; return pfx + '/' + mod; } const moduleStats = {}; function addModuleStat(side, r) { const key = side === 'nest' ? moduleKeyNest(r.file) : moduleKeyJava(r.file); if (!key) return; if (!moduleStats[key]) moduleStats[key] = { nest: 0, java: 0, missing_in_nest: 0, missing_in_java: 0, samples: [] }; moduleStats[key][side]++; } nestRoutes.forEach(r => addModuleStat('nest', r)); javaRoutes.forEach(r => addModuleStat('java', r)); diffs.forEach(d => { if (d.type === 'missing_in_nest') { const jr = d.java; const key = moduleKeyJava(jr.file); if (!key) return; moduleStats[key].missing_in_nest++; if (moduleStats[key].samples.length < 3) moduleStats[key].samples.push({ type: d.type, route: d.expected, file: jr.file, line: jr.line }); } else { const nr = d.nest; const key = moduleKeyNest(nr.file); if (!key) return; moduleStats[key].missing_in_java++; if (moduleStats[key].samples.length < 3) moduleStats[key].samples.push({ type: d.type, route: d.expected, file: nr.file, line: nr.line }); } }); const outJson = { summary, nestRoutes, javaRoutes, diffs, modules: moduleStats, }; const outDir = path.join(root, 'docs'); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync(path.join(outDir, 'routes-full-report.json'), JSON.stringify(outJson, null, 2)); const md = []; md.push('# Routes Full Report'); md.push(''); md.push('## Summary'); md.push(`- Nest routes: ${summary.counts.nest}`); md.push(`- Java routes: ${summary.counts.java}`); md.push(''); md.push('## Differences'); for (const d of diffs) { if (d.type === 'missing_in_nest') { const jr = d.java; md.push(`- 缺失于 v1: \`${d.expected}\` 证据: ${jr.file}:${jr.line}`); } else { const nr = d.nest; md.push(`- 缺失于 Java: \`${d.expected}\` 证据: ${nr.file}:${nr.line}`); } } md.push(''); md.push('## Sample (Top 50 Nest routes)'); nestRoutes.slice(0, 50).forEach(r => { md.push(`- ${r.httpMethod} ${fullPath(r)} — ${r.file}:${r.line}`); }); md.push(''); md.push('## Sample (Top 50 Java routes)'); javaRoutes.slice(0, 50).forEach(r => { md.push(`- ${r.httpMethod} ${fullPath(r)} — ${r.file}:${r.line}`); }); fs.writeFileSync(path.join(outDir, 'routes-full-report.md'), md.join('\n')); const md2 = []; md2.push('# Routes Modules Report'); md2.push(''); md2.push('## Summary'); md2.push(`- Nest routes: ${summary.counts.nest}`); md2.push(`- Java routes: ${summary.counts.java}`); md2.push(''); md2.push('## Modules (sorted by missing_in_nest + missing_in_java)'); const modEntries = Object.entries(moduleStats).sort((a,b)=>{ const av = a[1].missing_in_nest + a[1].missing_in_java; const bv = b[1].missing_in_nest + b[1].missing_in_java; return bv - av; }); modEntries.forEach(([key, s]) => { md2.push(`- ${key} — nest ${s.nest}, java ${s.java}, missing_in_nest ${s.missing_in_nest}, missing_in_java ${s.missing_in_java}`); s.samples.forEach(sm => { md2.push(` - ${sm.type} \`${sm.route}\` 证据: ${sm.file}:${sm.line}`); }); }); fs.writeFileSync(path.join(outDir, 'routes-modules-report.md'), md2.join('\n')); fs.writeFileSync(path.join(outDir, 'routes-modules-report.json'), JSON.stringify({ modules: moduleStats }, null, 2)); console.log('Generated docs/routes-full-report.{json,md}'); } buildReport();