7ea463ed08
保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义 移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖 恢复现役创作、项目、我的入口及桌面移动导航 收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界 补齐开发栈、网关、原生壳和文档退役约束
274 lines
7.8 KiB
JavaScript
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)`);
|