现象:生产 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 个输入框),两个业务路由正常回到登录引导,无致命错误。
71 lines
2.7 KiB
JavaScript
71 lines
2.7 KiB
JavaScript
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;
|
||
});
|
||
}
|