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; }); }