SPA冲突路由改名+旧路径Navigate重定向(/api-docs→/open-api-docs、/api-detail→/open-api-detail、/article→/post、/user→/user-home);vite代理宽前缀改精确正则白名单;nginx.conf同步同口径;新增86路由回归脚本+Playwright 3项(86/86全绿)。
82 lines
3.8 KiB
JavaScript
82 lines
3.8 KiB
JavaScript
#!/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 的 <Route path> 一一对应(不含 * 兜底)
|
||
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,由前端 <Navigate> 承接,不应被代理劫持)
|
||
'/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)
|