轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

102 lines
3.6 KiB

/**
* 路由审计脚本:交叉校验代码中引用的路由 vs app/ 下实际存在的路由文件。
* 用法:node scripts/check-routes.mjs
*/
import fs from 'fs';
import path from 'path';
const appDir = path.resolve('app');
const routeFiles = [];
function walk(dir, rel = '') {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
const relPath = rel ? rel + '/' + e.name : e.name;
if (e.isDirectory()) walk(full, relPath);
else if (/\.(tsx|ts|jsx|js)$/.test(e.name)) routeFiles.push(relPath);
}
}
walk(appDir);
function toRoute(file) {
let r = file.replace(/\.(tsx|ts|jsx|js)$/, '');
r = r.split('/').filter((s) => !/^\(.*\)$/.test(s)).join('/');
r = r.replace(/\/index$/, '');
return '/' + r.replace(/_layout$/, '').replace(/\/$/, '');
}
const actualRoutes = new Set(routeFiles.map(toRoute).filter(Boolean));
function normalize(t) {
const clean = t.split('?')[0].replace(/\/$/, '');
// 去掉路由组前缀:expo-router 允许写 "/(tabs)/jobs" 这类完整路径
return clean.split('/').filter((s) => s && !/^\(.*\)$/.test(s)).join('/');
}
function routeExists(target) {
const clean = target.split('?')[0].replace(/\/$/, '');
if (clean === '' || clean === 'index') return true;
if (actualRoutes.has(clean)) return true;
const n = normalize(clean);
for (const r of actualRoutes) {
const rn = normalize(r);
if (rn === n) return true;
const pattern =
'^' + rn.replace(/\[\[?\.\.\.(\w+)\]\]?/g, '.+').replace(/\[(\w+)\]/g, '[^/]+') + '$';
if (new RegExp(pattern).test(n)) return true;
}
return false;
}
const srcDirs = ['app', 'components', 'src'];
const files = [];
function walkSrc(dir) {
if (!fs.existsSync(dir)) return;
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, e.name);
if (e.isDirectory()) walkSrc(full);
else if (/\.(tsx|ts)$/.test(e.name)) files.push(full);
}
}
srcDirs.forEach(walkSrc);
const routesFound = new Map();
const reLocal = /['"`](\/[a-zA-Z0-9_()\-[\]/?=&.][a-zA-Z0-9_()\-[\]/?=&.]*)['"`]/g;
for (const f of files) {
const code = fs.readFileSync(f, 'utf8');
let m;
while ((m = reLocal.exec(code)) !== null) {
const r = m[1];
if (r.startsWith('//')) continue;
if (!routesFound.has(r)) routesFound.set(r, []);
routesFound.get(r).push(path.relative(process.cwd(), f).replace(/\\/g, '/'));
}
}
console.log('=== 实际存在的路由 (' + actualRoutes.size + ') ===');
console.log([...actualRoutes].sort().join('\n'));
console.log('\n=== 引用但不存在的路由 ===');
let bad = 0;
for (const [r, srcs] of [...routesFound.entries()].sort()) {
if (r.includes('${')) continue;
if (/\.(png|jpg|jpeg|svg|json|ttf|css)$/i.test(r)) continue;
if (r.startsWith('/node_modules')) continue;
if (!routeExists(r)) {
console.log(`${r} <-- ${[...new Set(srcs)].join(', ')}`);
bad++;
}
}
console.log('\n不存在的路由数量: ' + bad);
// 反向:孤立页面(没有任何入口指向)
console.log('\n=== 无入口指向的孤立页面 ===');
const referenced = new Set();
function markRef(r) {
referenced.add(r);
referenced.add('/' + normalize(r));
// 动态拼接的路径(如 '/chat/' + id)按前缀登记,前缀命中即视为已引用
const n = normalize(r);
const seg = n.split('/');
if (seg.length > 1) referenced.add('/' + seg.slice(0, seg.length - 1).join('/'));
}
for (const r of routesFound.keys()) markRef(r);
const orphans = [...actualRoutes].filter((r) => !referenced.has(r) && !referenced.has('/' + normalize(r)));
console.log(orphans.sort().join('\n') || '(无)');