chore: push latest changes
This commit is contained in:
320
scripts/generate-routes-report.js
Normal file
320
scripts/generate-routes-report.js
Normal file
@@ -0,0 +1,320 @@
|
||||
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] || '';
|
||||
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();
|
||||
Reference in New Issue
Block a user