文件
hyb-oa/scripts/qa/check-mobile-route-contract.mjs
T

179 行
5.2 KiB
JavaScript

#!/usr/bin/env node
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import path from "node:path";
import process from "node:process";
const rootDir = process.cwd();
const routerPath = path.join(rootDir, "client/src/router/index.tsx");
const pagesDir = path.join(rootDir, "client/src/pages");
const findings = [];
function toPosix(value) {
return value.split(path.sep).join("/");
}
function addFinding({ rule, file, line = 1, message, evidence }) {
findings.push({
rule,
file: toPosix(path.relative(rootDir, file)),
line,
message,
evidence,
});
}
function lineNumber(source, index) {
return source.slice(0, index).split("\n").length;
}
function collectFiles(dir) {
if (!existsSync(dir)) return [];
const files = [];
for (const entry of readdirSync(dir)) {
const fullPath = path.join(dir, entry);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
files.push(...collectFiles(fullPath));
continue;
}
if (/Mobile.*\.tsx$/.test(entry)) {
files.push(fullPath);
}
}
return files;
}
function extractRouteBlock(source, routePath) {
const marker = `path="${routePath}"`;
const start = source.indexOf(marker);
if (start < 0) return null;
const nextRoute = source.indexOf("\n <Route", start + marker.length);
const end = nextRoute >= 0 ? nextRoute : source.indexOf("\n </Route>", start);
return {
start,
block: source.slice(start, end >= 0 ? end : start + 1200),
};
}
function scanRouter() {
if (!existsSync(routerPath)) {
addFinding({
rule: "mobile.router.exists",
file: routerPath,
message: "找不到 client/src/router/index.tsx",
});
return;
}
const source = readFileSync(routerPath, "utf8");
const mobileRoutePaths = Array.from(source.matchAll(/path="(mobile[^"]*)"/g)).map((match) => match[1]);
const routeComponents = [];
for (const routePath of mobileRoutePaths) {
const route = extractRouteBlock(source, routePath);
if (!route) continue;
const components = Array.from(route.block.matchAll(/<([A-Z][A-Za-z0-9]+Page)\s*\/>/g)).map((match) => match[1]);
routeComponents.push({ routePath, components });
for (const component of components) {
if (!component.startsWith("Mobile")) {
addFinding({
rule: "mobile.router.no_pc_page",
file: routerPath,
line: lineNumber(source, route.start),
message: `/mobile 路由不能直挂 PC 页面组件:${routePath} -> ${component}`,
evidence: component,
});
}
}
}
const requiredRoutes = new Map([
["mobile/profile", "MobileProfilePage"],
["mobile/notifications", "MobileNotificationsPage"],
["mobile/capture", "MobileCapturePage"],
["mobile/reimbursements", "MobileReimbursementsPage"],
["mobile/me", "MobileMePage"],
["mobile/approvals", "MobileApprovalsPage"],
]);
for (const [routePath, expectedComponent] of requiredRoutes) {
const route = routeComponents.find((item) => item.routePath === routePath);
if (!route) {
addFinding({
rule: "mobile.router.required_route",
file: routerPath,
message: `缺少企业微信移动入口路由:${routePath}`,
});
continue;
}
if (!route.components.includes(expectedComponent)) {
addFinding({
rule: "mobile.router.required_component",
file: routerPath,
line: lineNumber(source, source.indexOf(`path="${routePath}"`)),
message: `${routePath} 应挂载 ${expectedComponent}`,
evidence: route.components.join(", ") || "(none)",
});
}
}
console.log("[mobile-route-contract] routes");
for (const item of routeComponents) {
console.log(` ${item.routePath} -> ${item.components.join(", ") || "(no page component found)"}`);
}
}
function scanMobilePages() {
for (const file of collectFiles(pagesDir)) {
const source = readFileSync(file, "utf8");
const patterns = [
{
regex: /\bfrom ["']\.\.\/components\/ui\/table["']/g,
rule: "mobile.page.no_table_component",
message: "移动页面不应导入桌面表格组件。",
},
{
regex: /<\s*(?:Table|table)\b/g,
rule: "mobile.page.no_visible_table_markup",
message: "移动页面不应渲染桌面表格。",
},
{
regex: /page-shell-grid/g,
rule: "mobile.page.no_pc_shell_grid",
message: "移动页面不应使用 PC 页面双栏 shell 布局。",
},
];
for (const pattern of patterns) {
let match = pattern.regex.exec(source);
while (match) {
addFinding({
rule: pattern.rule,
file,
line: lineNumber(source, match.index),
message: pattern.message,
evidence: match[0],
});
match = pattern.regex.exec(source);
}
}
}
}
scanRouter();
scanMobilePages();
if (findings.length) {
console.log(`[mobile-route-contract] ${findings.length} finding(s)`);
for (const finding of findings) {
console.log(`ERROR ${finding.rule} ${finding.file}:${finding.line} ${finding.message}`);
if (finding.evidence) {
console.log(` evidence: ${finding.evidence}`);
}
}
process.exitCode = 1;
} else {
console.log("[mobile-route-contract] no findings");
}