Files
HHY cf8c0c65ce fix: v2.2 前端完整性治理(vue-tsc 全量清零)
- 组合画布 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 全部通过
2026-09-21 04:27:16 -07:00

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} 个前端调用缺少后端路由`);