Files
Genarrative/scripts/check-pingora-route-parity.mjs
T
kdletters 7ea463ed08 退役旧创作模板业务并保留数据壳
保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义
移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖
恢复现役创作、项目、我的入口及桌面移动导航
收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界
补齐开发栈、网关、原生壳和文档退役约束
2026-07-18 22:02:22 +08:00

274 lines
7.8 KiB
JavaScript

#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import { expectedMainSpaRoutes } from './check-nginx-spa-routes.mjs';
const MATRIX_PATH = 'deploy/pingora/nginx-route-parity.matrix.json';
const PRODUCTION_NGINX_PATH = 'deploy/nginx/genarrative.conf';
const DEVELOPMENT_NGINX_PATH = 'deploy/nginx/genarrative-dev-http.conf';
const PINGORA_DOC_PATH =
'docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md';
const PINGORA_GATEWAY_SOURCE = 'server-rs/crates/pingora-gateway/src/main.rs';
const VALID_KINDS = new Set([
'proxy',
'static',
'redirect_permanent',
'shadow_probe',
'not_found',
]);
const VALID_PROXY_TARGETS = new Set(['api', 'spacetime']);
const VALID_STATIC_ROOTS = new Set(['web', 'acme']);
const VALID_STATIC_MODES = new Set(['exact', 'spa_fallback']);
const VALID_PROTECTION_CLASSES = new Set([
'admin_api',
'api',
'spacetime',
]);
const REQUIRED_ROUTE_IDS = [
'acme_challenge',
'shadow_probe',
'admin_redirect',
'admin_api_proxy',
'admin_assets',
'admin_spa_fallback',
'web_assets',
'generic_api_proxy',
'spacetime_subscribe',
'spacetime_identity',
'v1_forbidden',
'healthz_forbidden',
'readyz_forbidden',
'generated_assets_forbidden',
'web_spa_fallback',
'profile_spa_fallback',
'web_spa_case_trailing_slash',
'web_unknown_path_exact',
'creation_unknown_path_exact',
'runtime_unknown_path_exact',
'puzzle_unknown_path_exact',
];
const files = {
production: readFileSync(PRODUCTION_NGINX_PATH, 'utf8'),
development: readFileSync(DEVELOPMENT_NGINX_PATH, 'utf8'),
};
const docs = readFileSync(PINGORA_DOC_PATH, 'utf8');
const pingoraGatewaySource = readFileSync(PINGORA_GATEWAY_SOURCE, 'utf8');
const matrix = JSON.parse(readFileSync(MATRIX_PATH, 'utf8'));
const failures = [];
function fail(message) {
failures.push(message);
}
function hasOwn(object, key) {
return Object.prototype.hasOwnProperty.call(object, key);
}
function requireString(value, context) {
if (typeof value !== 'string' || value.trim() === '') {
fail(`${context} 必须是非空字符串。`);
return false;
}
return true;
}
function validateExpectation(route) {
const context = `${MATRIX_PATH} route ${route.id}`;
const expect = route.expect;
if (!expect || typeof expect !== 'object' || Array.isArray(expect)) {
fail(`${context} 缺少 expect 对象。`);
return;
}
if (!VALID_KINDS.has(expect.kind)) {
fail(`${context} expect.kind 不支持: ${expect.kind}`);
return;
}
if (expect.kind === 'proxy') {
if (!VALID_PROXY_TARGETS.has(expect.target)) {
fail(`${context} proxy target 不支持: ${expect.target}`);
}
if (
hasOwn(expect, 'bodyLimit') &&
expect.bodyLimit !== null &&
expect.bodyLimit !== 'default' &&
(!Number.isInteger(expect.bodyLimit) || expect.bodyLimit < 1)
) {
fail(`${context} bodyLimit 必须是 null、default 或正整数。`);
}
if (!VALID_PROTECTION_CLASSES.has(expect.protectionClass)) {
fail(
`${context} proxy protectionClass 不支持: ${expect.protectionClass}`,
);
}
return;
}
if (hasOwn(expect, 'protectionClass')) {
fail(`${context} 非 proxy 路由不能配置 protectionClass。`);
}
if (expect.kind === 'static') {
if (!VALID_STATIC_ROOTS.has(expect.root)) {
fail(`${context} static root 不支持: ${expect.root}`);
}
if (!VALID_STATIC_MODES.has(expect.mode)) {
fail(`${context} static mode 不支持: ${expect.mode}`);
}
}
if (
expect.kind === 'redirect_permanent' &&
!requireString(expect.location, `${context} redirect location`)
) {
fail(`${context} redirect_permanent 必须配置 location。`);
}
}
function validateNginxFragments(route) {
for (const environment of ['production', 'development']) {
const fragments = route.nginx?.[environment];
if (fragments === undefined) {
if (environment === 'production' && route.id !== 'shadow_probe') {
fail(`${MATRIX_PATH} route ${route.id} 缺少 production Nginx 片段。`);
}
continue;
}
if (!Array.isArray(fragments) || fragments.length === 0) {
fail(
`${MATRIX_PATH} route ${route.id}${environment} Nginx 片段不能为空。`,
);
continue;
}
for (const fragment of fragments) {
if (!requireString(fragment, `${route.id} ${environment} Nginx 片段`)) {
continue;
}
if (!files[environment].includes(fragment)) {
fail(
`${environment} Nginx 模板缺少 route ${route.id} 片段: ${fragment}`,
);
}
}
}
}
function validateDocFragments(route) {
if (!Array.isArray(route.docs) || route.docs.length === 0) {
fail(`${MATRIX_PATH} route ${route.id} 缺少 docs 片段。`);
return;
}
for (const fragment of route.docs) {
if (!requireString(fragment, `${route.id} docs 片段`)) {
continue;
}
if (!docs.includes(fragment)) {
fail(`Pingora 试点文档缺少 route ${route.id} 片段: ${fragment}`);
}
}
}
function validateMatrixShape() {
if (matrix.version !== 1) {
fail(`${MATRIX_PATH} version 必须为 1。`);
}
if (!Array.isArray(matrix.routes) || matrix.routes.length === 0) {
fail(`${MATRIX_PATH} routes 不能为空。`);
return;
}
const ids = new Set();
const samplePaths = new Set();
for (const route of matrix.routes) {
if (!requireString(route.id, `${MATRIX_PATH} route.id`)) {
continue;
}
if (ids.has(route.id)) {
fail(`${MATRIX_PATH} route id 重复: ${route.id}`);
}
ids.add(route.id);
if (!requireString(route.samplePath, `${route.id} samplePath`)) {
continue;
}
if (!route.samplePath.startsWith('/')) {
fail(`${MATRIX_PATH} route ${route.id} samplePath 必须以 / 开头。`);
}
if (samplePaths.has(route.samplePath)) {
fail(`${MATRIX_PATH} samplePath 重复: ${route.samplePath}`);
}
samplePaths.add(route.samplePath);
validateExpectation(route);
validateNginxFragments(route);
validateDocFragments(route);
}
for (const routeId of REQUIRED_ROUTE_IDS) {
if (!ids.has(routeId)) {
fail(`${MATRIX_PATH} 缺少必需 route id: ${routeId}`);
}
}
}
function validateRustTestUsesMatrix() {
for (const fragment of [
'include_str!("../../../../deploy/pingora/nginx-route-parity.matrix.json")',
'serde_json::from_str(ROUTE_PARITY_MATRIX_JSON)',
'protection_class_for_route(&route, &case.sample_path)',
'fn matches_nginx_route_parity_matrix()',
'fn is_main_spa_path(path: &str)',
"path.strip_suffix('/')",
'normalized.eq_ignore_ascii_case(candidate)',
]) {
if (!pingoraGatewaySource.includes(fragment)) {
fail(`Pingora Rust 路由 parity 测试缺少矩阵接入片段: ${fragment}`);
}
}
}
function validateRustMainSpaRoutes() {
const routeBlock = pingoraGatewaySource.match(
/const MAIN_SPA_PATHS: &\[&str\] = &\[([\s\S]*?)\];/u,
);
if (!routeBlock) {
fail('Pingora Rust 缺少 MAIN_SPA_PATHS allowlist。');
return;
}
const rustRoutes = Array.from(
routeBlock[1].matchAll(/"([^"]+)"/gu),
(match) => match[1],
).sort();
const expected = new Set(expectedMainSpaRoutes);
const actual = new Set(rustRoutes);
const missing = expectedMainSpaRoutes.filter((route) => !actual.has(route));
const extra = rustRoutes.filter((route) => !expected.has(route));
if (missing.length > 0) {
fail(`Pingora MAIN_SPA_PATHS 缺少当前前端路由: ${missing.join(', ')}`);
}
if (extra.length > 0) {
fail(`Pingora MAIN_SPA_PATHS 包含非当前前端路由: ${extra.join(', ')}`);
}
}
validateMatrixShape();
validateRustTestUsesMatrix();
validateRustMainSpaRoutes();
if (failures.length > 0) {
console.error('[check:pingora-route-parity] FAILED');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log(`[check:pingora-route-parity] OK (${matrix.routes.length} routes)`);