feat(C-01):路由/代理前缀冲突修复
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全绿)。
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user