cangku/frontend/scripts/verify-router-lazy.mjs
yanhui 9b1ec36c70 fix(router): 补回 BinReplenishMonitor 的 lazy 声明,修复全站白屏
现象:生产 https://ck.yydd.ai:8443 打开任意页面都是白屏,连登录页都进不去。
控制台报 `BinReplenishMonitor is not defined`,一个 /api 请求都没发出去。

根因:PR #74 新增「缺货补货监控」时,在 router/index.jsx 的路由表里写了
`element: renderLazyComponent(BinReplenishMonitor)`,但没有像其它 22 个页面那样
在文件顶部补 `const BinReplenishMonitor = lazy(() => import('../pages/BinReplenishMonitor'))`。
该行在模块顶层求值 → 加载即抛 ReferenceError → 整个 React 应用挂掉。
`vite build` 不会报错(未声明标识符在 JS 里是合法语法),所以一路构建通过、部署后白屏。
PR #75 只补了后端的 binReplenishRoutes 引入,前端的这处漏声明一直没人发现。

修复:
1. router/index.jsx 补上 lazy 声明,并留下注释说明「漏一个就是全站白屏」。
2. 新增 frontend/scripts/verify-router-lazy.mjs,静态扫描 router/index.jsx:
   凡是 renderLazyComponent(X) 用到的 X,必须在同文件有 `const X = lazy(` 声明,
   否则构建直接失败并指出缺少的组件名与行号。已挂到 `npm run build` 前置。

验证:
- 该检查对修复前的 origin/main 版本报错并准确指出
  `BinReplenishMonitor (router/index.jsx:143)`;对修复后版本通过(23 used / 23 declared)。
- eslint 通过;vite build 成功(4270 modules / 34s)。
- playwright 真渲染:修复前 /login 白屏 + pageerror;修复后 /login 完整渲染登录表单
  (123 字、3 个输入框),两个业务路由正常回到登录引导,无致命错误。
2026-10-22 12:06:48 +08:00

71 lines
2.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { readFile } from 'node:fs/promises';
import path from 'node:path';
import process from 'node:process';
import { pathToFileURL } from 'node:url';
/**
* 校验路由表里每个 renderLazyComponent(X) 用到的组件 X 都在同文件里
* 用 `const X = lazy(() => import(...))` 声明过。
*
* 为什么要这个检查(2026-10-22 生产事故):
* PR #74 新增「缺货补货监控」时,router/index.jsx 里写了
* `element: renderLazyComponent(BinReplenishMonitor)` 却漏了 lazy 声明。
* 这行在模块顶层执行 → 加载即抛 `ReferenceError: BinReplenishMonitor is not defined`
* → 整个 React 应用挂掉、**全站白屏,连登录页都渲染不出来**。
* 而 `vite build` 完全能构建成功(未声明标识符在 JS 里是合法语法),
* 所以必须在构建期用静态扫描拦住。
*/
export async function verifyRouterLazy(routerPath = path.resolve(process.cwd(), 'src/router/index.jsx')) {
const source = await readFile(routerPath, 'utf8');
const used = new Set();
for (const match of source.matchAll(/renderLazyComponent\s*\(\s*([A-Za-z_$][\w$]*)/g)) {
used.add(match[1]);
}
const declared = new Set();
for (const match of source.matchAll(/const\s+([A-Za-z_$][\w$]*)\s*=\s*lazy\s*\(/g)) {
declared.add(match[1]);
}
const missing = [...used].filter((name) => !declared.has(name)).sort();
if (missing.length > 0) {
const lines = missing.map((name) => {
const idx = source.indexOf(`renderLazyComponent(${name}`);
const line = idx === -1 ? '?' : source.slice(0, idx).split('\n').length;
return ` - ${name} (router/index.jsx:${line} 使用,缺少 const ${name} = lazy(() => import('...')) 声明)`;
});
throw new Error(
'router/index.jsx 有组件被 renderLazyComponent 使用但未声明为 lazy,' +
'这会导致运行时 ReferenceError 并让整站白屏:\n' +
lines.join('\n'),
);
}
return { routerPath, used: [...used].sort(), declared: [...declared].sort() };
}
async function main() {
const routerArg = process.argv[2];
const routerPath = routerArg
? path.resolve(process.cwd(), routerArg)
: path.resolve(process.cwd(), 'src/router/index.jsx');
const result = await verifyRouterLazy(routerPath);
process.stdout.write(
`Verified router lazy declarations: ${result.used.length} used, ${result.declared.length} declared ` +
`(${path.relative(process.cwd(), result.routerPath)})\n`,
);
}
const isDirectExecution = process.argv[1]
? pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url
: false;
if (isDirectExecution) {
main().catch((error) => {
process.stderr.write(`${error.message}\n`);
process.exitCode = 1;
});
}