refactor: checker 模块内聚化 — 每个 checker 自包含于独立目录
将 checker 架构重构为完全内聚模式:每个 checker 目录包含自身的 types、schema、validate、execute、expect 和 index,新增 checker 只需创建一个目录并在 runner/index.ts 添加一行注册。 主要变更: - runner/shared/ 拆分:断言基础设施迁入 checker/expect/, body.ts 迁入 http/,text.ts 迁入 command/ - config-contract/ 重命名为 schema/,schema.ts → builder.ts - size.ts + parseDuration 合并为 utils.ts - 顶层 types.ts 改为 base interface + index signature, checker 专属类型下沉到各自 types.ts - runner/index.ts 改为显式数组注册模式 - 更新 DEVELOPMENT.md 项目结构和开发新 Checker 指南
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
import type { ConfigValidationIssue } from "./config-contract/issues";
|
||||
import type { DefaultsConfig, EngineRuntimeConfig, ResolvedTarget, TargetConfig } from "./types";
|
||||
import type { ConfigValidationIssue } from "./schema/issues";
|
||||
import type { DefaultsConfig, EngineRuntimeConfig, RawTargetConfig, ResolvedTargetBase } from "./types";
|
||||
|
||||
import { issue, throwConfigIssues } from "./config-contract/issues";
|
||||
import { asValidatedConfig, type RawProbeConfig } from "./config-contract/types";
|
||||
import { validateProbeConfigContract } from "./config-contract/validate";
|
||||
import { checkerRegistry } from "./runner";
|
||||
import { issue, throwConfigIssues } from "./schema/issues";
|
||||
import { asValidatedConfig, type RawProbeConfig } from "./schema/types";
|
||||
import { validateProbeConfigContract } from "./schema/validate";
|
||||
import { parseDuration } from "./utils";
|
||||
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const DEFAULT_PORT = 3000;
|
||||
@@ -21,7 +22,7 @@ export interface ResolvedConfig {
|
||||
host: string;
|
||||
maxConcurrentChecks: number;
|
||||
port: number;
|
||||
targets: ResolvedTarget[];
|
||||
targets: ResolvedTargetBase[];
|
||||
}
|
||||
|
||||
export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
|
||||
@@ -76,13 +77,35 @@ export async function loadConfig(configPath: string): Promise<ResolvedConfig> {
|
||||
const defaultIntervalMs = parseDuration(defaults.interval ?? DEFAULT_INTERVAL);
|
||||
const defaultTimeoutMs = parseDuration(defaults.timeout ?? DEFAULT_TIMEOUT);
|
||||
|
||||
const targets: ResolvedTarget[] = validated.targets.map((target) =>
|
||||
const targets: ResolvedTargetBase[] = validated.targets.map((target) =>
|
||||
resolveTarget(target, defaults, defaultIntervalMs, defaultTimeoutMs, configDir),
|
||||
);
|
||||
|
||||
return { configDir, dataDir, host, maxConcurrentChecks, port, targets };
|
||||
}
|
||||
|
||||
function canRunSemanticValidation(value: unknown): boolean {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function dedupeIssues(issues: ConfigValidationIssue[]): ConfigValidationIssue[] {
|
||||
const seen = new Set<string>();
|
||||
const result: ConfigValidationIssue[] = [];
|
||||
for (const item of issues) {
|
||||
const key = `${item.code}:${item.path}:${item.message}:${item.targetName ?? ""}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export { parseDuration } from "./utils";
|
||||
|
||||
function resolveMaxConcurrentChecks(runtime: EngineRuntimeConfig): number {
|
||||
if (runtime.maxConcurrentChecks === undefined) return DEFAULT_MAX_CONCURRENT_CHECKS;
|
||||
if (
|
||||
@@ -95,12 +118,12 @@ function resolveMaxConcurrentChecks(runtime: EngineRuntimeConfig): number {
|
||||
}
|
||||
|
||||
function resolveTarget(
|
||||
target: TargetConfig,
|
||||
target: RawTargetConfig,
|
||||
defaults: DefaultsConfig,
|
||||
defaultIntervalMs: number,
|
||||
defaultTimeoutMs: number,
|
||||
configDir: string,
|
||||
): ResolvedTarget {
|
||||
): ResolvedTargetBase {
|
||||
const intervalMs = parseDuration(target.interval ?? defaults.interval ?? DEFAULT_INTERVAL);
|
||||
const timeoutMs = parseDuration(target.timeout ?? defaults.timeout ?? DEFAULT_TIMEOUT);
|
||||
|
||||
@@ -192,44 +215,6 @@ function validateConfig(config: RawProbeConfig): ConfigValidationIssue[] {
|
||||
return issues;
|
||||
}
|
||||
|
||||
const DURATION_REGEX = /^(\d+(?:\.\d+)?)(ms|s|m)$/;
|
||||
|
||||
export function parseDuration(value: string): number {
|
||||
const match = DURATION_REGEX.exec(value);
|
||||
if (!match) {
|
||||
throw new Error(`无效的时长格式: "${value}",支持格式如 "30s"、"5m"、"500ms"`);
|
||||
}
|
||||
|
||||
const num = parseFloat(match[1]!);
|
||||
const unit = match[2]!;
|
||||
|
||||
const durationMs = unit === "ms" ? num : unit === "s" ? num * 1000 : num * 60 * 1000;
|
||||
if (!Number.isInteger(durationMs) || durationMs <= 0 || !Number.isFinite(durationMs)) {
|
||||
throw new Error(`无效的时长格式: "${value}",解析结果必须为正整数毫秒`);
|
||||
}
|
||||
return durationMs;
|
||||
}
|
||||
|
||||
function canRunSemanticValidation(value: unknown): boolean {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function dedupeIssues(issues: ConfigValidationIssue[]): ConfigValidationIssue[] {
|
||||
const seen = new Set<string>();
|
||||
const result: ConfigValidationIssue[] = [];
|
||||
for (const item of issues) {
|
||||
const key = `${item.code}:${item.path}:${item.message}:${item.targetName ?? ""}`;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
result.push(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function validateDurationValue(
|
||||
value: string | undefined,
|
||||
path: string,
|
||||
|
||||
Reference in New Issue
Block a user