diff --git a/frontend/package.json b/frontend/package.json index 6b5c08b..1d159a8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,8 +7,9 @@ "test": "node --test test/*.test.mjs", "prebuild": "node ./scripts/write-build-version.mjs", "dev": "node ./scripts/write-build-version.mjs && vite --configLoader native", - "build": "vite build && node ./scripts/verify-planning-entry.mjs", + "build": "node ./scripts/verify-router-lazy.mjs && vite build && node ./scripts/verify-planning-entry.mjs", "check": "npm run lint && npm test && npm run build", + "verify:router-lazy": "node ./scripts/verify-router-lazy.mjs", "verify:public-build": "node ./scripts/verify-public-build.mjs", "lint": "eslint .", "preview": "vite preview" diff --git a/frontend/scripts/verify-router-lazy.mjs b/frontend/scripts/verify-router-lazy.mjs new file mode 100644 index 0000000..ff7f704 --- /dev/null +++ b/frontend/scripts/verify-router-lazy.mjs @@ -0,0 +1,70 @@ +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; + }); +} diff --git a/frontend/src/router/index.jsx b/frontend/src/router/index.jsx index a6ecd7d..6a023db 100644 --- a/frontend/src/router/index.jsx +++ b/frontend/src/router/index.jsx @@ -25,6 +25,10 @@ const DailyInventory = lazy(() => import('../pages/DailyInventory')); const StockSalesAnalysis = lazy(() => import('../pages/StockSalesAnalysis')); const PickingBinSalesAnalysis = lazy(() => import('../pages/PickingBinSalesAnalysis')); const ReturnsInventoryMonitor = lazy(() => import('../pages/ReturnsInventoryWarning/Monitor')); +// ⚠️ 每个在下面路由表里被 renderLazyComponent(...) 用到的页面都必须先在这里声明。 +// 漏一个 = 模块顶层抛 ReferenceError(xxx is not defined)→ 整个应用白屏、连登录页都进不去。 +// PR #74 就漏了 BinReplenishMonitor,导致生产 10-22 全站白屏(2026-10-22 修复)。 +const BinReplenishMonitor = lazy(() => import('../pages/BinReplenishMonitor')); const lazyLoadingElement = ;