#!/usr/bin/env node /* C-01 路由回归脚本:73 条 SPA 路由直访检查。 * 检测三件套:① Django 404/JSON 劫持 ② #root 空(白屏)③ 正文长度。 * 用法:node scripts/check_routes.mjs --base http://127.0.0.1:5173 * 期望:PASS 73/73,hijacked=0,blank=0 */ import { readFileSync } from 'node:fs' const args = process.argv.slice(2) const baseIdx = args.indexOf('--base') const BASE = baseIdx >= 0 && args[baseIdx + 1] ? args[baseIdx + 1].replace(/\/$/, '') : 'http://127.0.0.1:5173' // 与 src/App.tsx 的 一一对应(不含 * 兜底) const ROUTES = [ '/', '/settings', '/history', '/favorites', '/open-api-docs', '/open-api-directory', '/profile', '/profile/email', '/profile/phone', '/profile/change-password', '/use', '/document', '/open-api', '/utility', '/learn', '/articles', '/bug', '/search', '/messages', '/chat', '/open-api-detail/1', '/tool-detail', '/course-learn', '/post/1', '/privacy', '/agreement', '/manual', '/system-message', '/user-home/1', '/user-home', '/console', '/article-manage', '/bug-detail', '/article-editor', '/learn-manage', '/learn-editor', '/baidu-translate', '/wallet/points', '/wallet/coins', '/wallet/coins/recharge', '/task-center', '/wallet/invite', '/invite/abc123', '/forgot-password', '/register', '/utility/json-formatter', '/utility/base64', '/utility/timestamp', '/utility/regex', '/utility/code-runner', '/utility/text-diff', '/utility/encoding-converter', '/utility/charset-converter', '/utility/color-converter', '/utility/code-formatter', '/color-picker', '/qrcode-generator', '/utility/image-compressor', '/utility/calculator', '/utility/date-calculator', '/utility/image-editor', '/changelog', '/ip-location', '/currency-detail', '/open-api-detail/currency', '/open-api-detail/baidu-translate', '/open-api/air-quality-details', '/open-api/weather-details', '/open-api/qrcode-generator', '/open-api/ip-location', '/open-api/password-generator', '/shorturl-detail', // 旧路径重定向(应返回 SPA,由前端 承接,不应被代理劫持) '/api-docs', '/api-directory', '/api', '/api-detail/1', '/api-detail/currency', '/api-detail/baidu-translate', '/api/air-quality-details', '/api/weather-details', '/api/qrcode-generator', '/api/ip-location', '/api/password-generator', '/article/1', '/user', '/user/1', ] function isHijacked(status, text) { if (status === 404 && /Django|Not Found|Page not found/i.test(text)) return true const t = text.trim() // 后端 JSON 特征:{"code":...} / {"detail":...} 且无前端根节点 if (/^\s*\{[\s\S]*"(code|detail|message)"[\s\S]*\}\s*$/.test(t.slice(0, 2000)) && !text.includes('id="root"')) return true return false } // dev 模式下 index.html 的 #root 恒为空(客户端水合前), // 因此"白屏"只能断言"是否返回了 SPA 外壳":含 id="root" 即算 SPA 到达。 // 真正的渲染白屏由 Playwright 抽查覆盖(见 PROGRESS)。 function isSpaShell(text) { return text.includes('id="root"') } let pass = 0, hijacked = 0, blank = 0 const bad = [] for (const r of ROUTES) { const url = BASE + r try { const res = await fetch(url, { redirect: 'manual' }) const text = await res.text() const hj = isHijacked(res.status, text) const bl = !hj && !isSpaShell(text) if (hj) hijacked++ if (bl) blank++ if (!hj && !bl) { pass++; console.log(`ok ${r}`) } else { bad.push(r); console.log(`${hj ? 'HIJACKED' : 'BLANK '} ${r} (status=${res.status} len=${text.length})`) } } catch (e) { bad.push(r) console.log(`ERROR ${r} (${e.message})`) } } console.log('---') console.log(`ROUTES=${ROUTES.length} PASS=${pass} hijacked=${hijacked} blank=${blank}`) if (bad.length) { console.log('BAD:'); bad.forEach((r) => console.log(' - ' + r)) } process.exit(pass === ROUTES.length ? 0 : 1)