Files
wwjcloud-nest-v1/scripts/generate-routes-report.js
wanwu 6eb9ea687d feat: 初始化项目代码
- 迁移 NestJS 项目结构
- 添加 uniappx 前端代码
- 配置数据库连接
- 添加核心业务模块
2026-04-02 21:25:02 +08:00

329 lines
11 KiB
JavaScript

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();