2025-11-16 22:13:57 +08:00
|
|
|
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] || '';
|
2026-04-02 21:25:02 +08:00
|
|
|
|
|
|
|
|
// 对于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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-16 22:13:57 +08:00
|
|
|
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();
|