- 组合画布 3D 扇面板解构键名错误(rect/s→rect/sash)——3D 混乱根因之一 - WindoorCanvas isDoor 标注 cx 作用域泄漏修复 - WindoorThreed rebuild 函数定义补全(墙体开关可用) - 量尺页 openCreate 补定义(点击不再报错) - 状态筛选 ref 类型纳入空串语义(4 页面) - CombineCanvas 孤儿 @drop 移除+容器 dragover.prevent(拖放入库修复) - AIRenderCanvas style 属性名保留处理+表单联合类型 - 零散隐式 any 参数补类型 - 新增 check-frontend.cjs: 未定义引用扫描+API 路径一致性比对 - 全量回归: 18 GET+4 POST+7 SPA 全部通过
95 lines
4.2 KiB
JavaScript
95 lines
4.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/** 前端静态检查: ①SFC 脚本未定义引用扫描 ②前端 API 路径与后端路由一致性比对 */
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const SRC = path.join(ROOT, 'src');
|
|
const API = path.join(ROOT, '..', 'src', 'huamenchuang', 'api');
|
|
|
|
const BUILTIN = new Set(['if','for','while','switch','catch','return','function','typeof','new','delete','void',
|
|
'fetch','setTimeout','setInterval','clearTimeout','clearInterval','parseInt','parseFloat','isNaN','isFinite',
|
|
'Number','String','Boolean','Array','Object','JSON','Math','Date','Promise','console','window','document',
|
|
'localStorage','sessionStorage','navigator','location','alert','requestAnimationFrame','cancelAnimationFrame',
|
|
'URL','Blob','File','FileReader','Error','TypeError','structuredClone','crypto','atob','btoa','history',
|
|
'defineProps','defineEmits','defineExpose','withDefaults','require','module','Infinity','NaN','encodeURIComponent','decodeURIComponent']);
|
|
const VUE = new Set(['ref','reactive','computed','watch','watchEffect','onMounted','onBeforeUnmount','onUnmounted','nextTick',
|
|
'createApp','useRouter','useRoute','toRefs','toRef','unref','isRef','provide','inject','defineStore','storeToRefs']);
|
|
|
|
function walk(dir, out = []) {
|
|
for (const f of fs.readdirSync(dir)) {
|
|
const p = path.join(dir, f);
|
|
if (fs.statSync(p).isDirectory()) walk(p, out);
|
|
else out.push(p);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// ---- ① SFC 未定义引用 ----
|
|
console.log('=== ① SFC 脚本未定义引用扫描 ===');
|
|
const vueFiles = walk(SRC).filter(f => f.endsWith('.vue'));
|
|
let issues = 0;
|
|
for (const f of vueFiles) {
|
|
const src = fs.readFileSync(f, 'utf8');
|
|
const m = src.match(/<script setup lang="ts">([\s\S]*?)<\/script>/);
|
|
if (!m) continue;
|
|
const script = m[1];
|
|
const declared = new Set([...BUILTIN, ...VUE]);
|
|
// import 绑定
|
|
for (const im of script.matchAll(/import\s+([^;]+?)\s+from/g)) {
|
|
for (const bind of im[1].split(',')) {
|
|
const name = bind.trim().split(/\s+as\s+/).pop().trim().replace(/[{}]/g, '').trim();
|
|
if (name) declared.add(name);
|
|
}
|
|
}
|
|
// 声明
|
|
for (const d of script.matchAll(/\b(?:function|const|let|var)\s+([A-Za-z_$][\w$]*)/g)) declared.add(d[1]);
|
|
// 函数参数(粗略)
|
|
for (const p of script.matchAll(/\(([^()]{0,60}?)\)\s*(?:=>|{)/g)) {
|
|
for (const part of p[1].split(',')) {
|
|
const name = part.trim().replace(/^[{[]/, '').split(':')[0].split('=')[0].trim();
|
|
if (/^[A-Za-z_$][\w$]*$/.test(name)) declared.add(name);
|
|
}
|
|
}
|
|
for (const p of script.matchAll(/\(\s*([A-Za-z_$][\w$]*)\s*(?::[^)=]+)?\)\s*=>/g)) declared.add(p[1]);
|
|
// 调用点
|
|
for (const c of script.matchAll(/(?<![.\w$])([A-Za-z_$][\w$]*)\s*\(/g)) {
|
|
const name = c[1];
|
|
if (!declared.has(name)) {
|
|
const rel = path.relative(SRC, f);
|
|
console.log(` ${rel}: 调用了未定义的 "${name}"`);
|
|
issues++;
|
|
}
|
|
}
|
|
}
|
|
console.log(issues === 0 ? ' ✓ 未发现未定义调用' : ` ✗ 共 ${issues} 处`);
|
|
|
|
// ---- ② API 路径一致性 ----
|
|
console.log('\n=== ② 前端 API 路径 vs 后端路由 ===');
|
|
const fe = walk(SRC).filter(f => f.endsWith('.vue') || f.endsWith('.ts'));
|
|
const fePaths = new Set();
|
|
for (const f of fe) {
|
|
const src = fs.readFileSync(f, 'utf8');
|
|
for (const mm of src.matchAll(/['"`](\/api\/[^'"`?]*)/g)) fePaths.add(mm[1]);
|
|
}
|
|
const bePaths = new Set();
|
|
const beDir = API;
|
|
for (const f of fs.readdirSync(beDir).filter(f => f.endsWith('.py'))) {
|
|
const src = fs.readFileSync(path.join(beDir, f), 'utf8');
|
|
const prefixM = src.match(/router = APIRouter\(prefix="([^"]*)"/);
|
|
const prefix = prefixM ? prefixM[1] : '';
|
|
for (const r of src.matchAll(/@router\.(get|post|put|delete)\("([^"]*)"/g)) {
|
|
let p = prefix + r[2];
|
|
p = p.replace(/\{[^}]*\}/g, 'X');
|
|
bePaths.add(p);
|
|
}
|
|
}
|
|
let missing = 0;
|
|
for (const p of [...fePaths].sort()) {
|
|
const norm = p.replace(/\?.*$/, '').replace(/\/\d+/g, '/X');
|
|
if ([...bePaths].some(b => b === norm || norm.startsWith(b + '/'))) continue;
|
|
const found = [...bePaths].some(b => norm === b || norm.startsWith(b));
|
|
if (!found) { console.log(` 前端调用但后端无: ${p}`); missing++; }
|
|
}
|
|
console.log(missing === 0 ? ' ✓ 前端调用的接口后端全部存在' : ` ✗ ${missing} 个前端调用缺少后端路由`);
|