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,21 +1,16 @@
|
||||
import { isError } from "es-toolkit";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import type {
|
||||
CheckResult,
|
||||
CommandTargetConfig,
|
||||
ResolvedCommandTarget,
|
||||
ResolvedTarget,
|
||||
TargetConfig,
|
||||
} from "../../types";
|
||||
import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../../types";
|
||||
import type { Checker, CheckerContext, CheckerValidationInput, ResolveContext } from "../types";
|
||||
import type { CommandExpectConfig, CommandTargetConfig, ResolvedCommandTarget } from "./types";
|
||||
|
||||
import { parseSize } from "../../size";
|
||||
import { checkDuration } from "../shared/duration";
|
||||
import { errorFailure } from "../shared/failure";
|
||||
import { checkTextRules } from "../shared/text";
|
||||
import { commandCheckerSchemas } from "./contract";
|
||||
import { checkDuration } from "../../expect/duration";
|
||||
import { errorFailure } from "../../expect/failure";
|
||||
import { parseSize } from "../../utils";
|
||||
import { checkExitCode } from "./expect";
|
||||
import { commandCheckerSchemas } from "./schema";
|
||||
import { checkTextRules } from "./text";
|
||||
import { validateCommandConfig } from "./validate";
|
||||
|
||||
export class CommandChecker implements Checker {
|
||||
@@ -25,7 +20,7 @@ export class CommandChecker implements Checker {
|
||||
|
||||
readonly type = "command";
|
||||
|
||||
async execute(target: ResolvedTarget, ctx: CheckerContext): Promise<CheckResult> {
|
||||
async execute(target: ResolvedTargetBase, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const t = target as ResolvedCommandTarget;
|
||||
const timestamp = new Date().toISOString();
|
||||
const start = performance.now();
|
||||
@@ -174,9 +169,9 @@ export class CommandChecker implements Checker {
|
||||
};
|
||||
}
|
||||
|
||||
resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget {
|
||||
const t = target as TargetConfig & { command: CommandTargetConfig; type: "command" };
|
||||
const commandDefaults = context.defaults.command;
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTargetBase {
|
||||
const t = target as RawTargetConfig & { command: CommandTargetConfig; type: "command" };
|
||||
const commandDefaults = context.defaults["command"] as undefined | { cwd?: string; maxOutputBytes?: string };
|
||||
|
||||
const cwd = t.command.cwd ?? commandDefaults?.cwd ?? ".";
|
||||
const resolvedCwd = resolve(context.configDir, cwd);
|
||||
@@ -193,7 +188,7 @@ export class CommandChecker implements Checker {
|
||||
exec: t.command.exec,
|
||||
maxOutputBytes,
|
||||
},
|
||||
expect: target.expect,
|
||||
expect: target.expect as CommandExpectConfig | undefined,
|
||||
group: target.group ?? "default",
|
||||
intervalMs: context.defaultIntervalMs,
|
||||
name: t.name,
|
||||
@@ -202,7 +197,7 @@ export class CommandChecker implements Checker {
|
||||
} satisfies ResolvedCommandTarget;
|
||||
}
|
||||
|
||||
serialize(target: ResolvedTarget): { config: string; target: string } {
|
||||
serialize(target: ResolvedTargetBase): { config: string; target: string } {
|
||||
const t = target as ResolvedCommandTarget;
|
||||
const parts = [t.command.exec, ...t.command.args];
|
||||
return {
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ExpectResult } from "../shared/duration";
|
||||
import type { ExpectResult } from "../../expect/types";
|
||||
|
||||
import { mismatchFailure } from "../shared/failure";
|
||||
import { mismatchFailure } from "../../expect/failure";
|
||||
|
||||
export function checkExitCode(exitCode: number, allowed: number[]): ExpectResult {
|
||||
if (!allowed.includes(exitCode)) {
|
||||
|
||||
1
src/server/checker/runner/command/index.ts
Normal file
1
src/server/checker/runner/command/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CommandChecker } from "./execute";
|
||||
@@ -2,7 +2,7 @@ import { Type } from "@sinclair/typebox";
|
||||
|
||||
import type { CheckerSchemas } from "../types";
|
||||
|
||||
import { createTextRulesSchema, sizeSchema, stringMapSchema } from "../../config-contract/fragments";
|
||||
import { createTextRulesSchema, sizeSchema, stringMapSchema } from "../../schema/fragments";
|
||||
|
||||
export const commandCheckerSchemas: CheckerSchemas = {
|
||||
config: Type.Object(
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { TextRule } from "../../types";
|
||||
import type { ExpectResult } from "./duration";
|
||||
import type { ExpectResult } from "../../expect/types";
|
||||
import type { TextRule } from "./types";
|
||||
|
||||
import { mismatchFailure } from "./failure";
|
||||
import { applyOperator } from "./operator";
|
||||
import { mismatchFailure } from "../../expect/failure";
|
||||
import { applyOperator } from "../../expect/operator";
|
||||
|
||||
export function checkTextRules(text: string, rules: TextRule[], phase: string): ExpectResult {
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
41
src/server/checker/runner/command/types.ts
Normal file
41
src/server/checker/runner/command/types.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ExpectOperator, ResolvedTargetBase } from "../../types";
|
||||
|
||||
export interface CommandDefaultsConfig {
|
||||
cwd?: string;
|
||||
maxOutputBytes?: string;
|
||||
}
|
||||
|
||||
export interface CommandExpectConfig {
|
||||
exitCode?: number[];
|
||||
maxDurationMs?: number;
|
||||
stderr?: TextRule[];
|
||||
stdout?: TextRule[];
|
||||
}
|
||||
|
||||
export interface CommandTargetConfig {
|
||||
args?: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
exec: string;
|
||||
maxOutputBytes?: string;
|
||||
}
|
||||
|
||||
export interface ResolvedCommandConfig {
|
||||
args: string[];
|
||||
cwd: string;
|
||||
env: Record<string, string>;
|
||||
exec: string;
|
||||
maxOutputBytes: number;
|
||||
}
|
||||
|
||||
export interface ResolvedCommandTarget extends ResolvedTargetBase {
|
||||
command: ResolvedCommandConfig;
|
||||
expect?: CommandExpectConfig;
|
||||
group: string;
|
||||
intervalMs: number;
|
||||
name: string;
|
||||
timeoutMs: number;
|
||||
type: "command";
|
||||
}
|
||||
|
||||
export type TextRule = ExpectOperator;
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { ConfigValidationIssue } from "../../config-contract/issues";
|
||||
import type { ConfigValidationIssue } from "../../schema/issues";
|
||||
import type { CheckerValidationInput } from "../types";
|
||||
|
||||
import { issue, joinPath } from "../../config-contract/issues";
|
||||
import { parseSize } from "../../size";
|
||||
import { validateTextRules } from "../shared/validate";
|
||||
import { validateOperatorObject } from "../../expect/validate-operator";
|
||||
import { issue, joinPath } from "../../schema/issues";
|
||||
import { parseSize } from "../../utils";
|
||||
|
||||
export function validateCommandConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
@@ -91,3 +91,8 @@ function validateSizeValue(value: number | string, path: string, targetName?: st
|
||||
return [issue("invalid-size", path, error instanceof Error ? error.message : "size 格式不合法", targetName)];
|
||||
}
|
||||
}
|
||||
|
||||
function validateTextRules(rules: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!Array.isArray(rules)) return [issue("invalid-type", path, "必须为数组", targetName)];
|
||||
return rules.flatMap((rule, index) => validateOperatorObject(rule, `${path}[${index}]`, targetName));
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import { DOMParser } from "@xmldom/xmldom";
|
||||
import * as cheerio from "cheerio";
|
||||
import * as xpath from "xpath";
|
||||
|
||||
import type { BodyRule, CssRule, JsonRule, XpathRule } from "../../types";
|
||||
import type { ExpectResult } from "./duration";
|
||||
import type { ExpectResult } from "../../expect/types";
|
||||
import type { BodyRule, CssRule, JsonRule, XpathRule } from "./types";
|
||||
|
||||
import { errorFailure, mismatchFailure } from "./failure";
|
||||
import { applyOperator, evaluateJsonPath } from "./operator";
|
||||
import { errorFailure, mismatchFailure } from "../../expect/failure";
|
||||
import { applyOperator, evaluateJsonPath } from "../../expect/operator";
|
||||
|
||||
export function checkBodyExpect(body: string, rules?: BodyRule[]): ExpectResult {
|
||||
if (!rules || rules.length === 0) return { failure: null, matched: true };
|
||||
@@ -1,14 +1,15 @@
|
||||
import { isError } from "es-toolkit";
|
||||
|
||||
import type { CheckResult, HttpTargetConfig, ResolvedHttpTarget, ResolvedTarget, TargetConfig } from "../../types";
|
||||
import type { CheckResult, RawTargetConfig, ResolvedTargetBase } from "../../types";
|
||||
import type { Checker, CheckerContext, CheckerValidationInput, ResolveContext } from "../types";
|
||||
import type { HttpExpectConfig, HttpTargetConfig, ResolvedHttpTarget } from "./types";
|
||||
|
||||
import { parseSize } from "../../size";
|
||||
import { checkBodyExpect } from "../shared/body";
|
||||
import { checkDuration } from "../shared/duration";
|
||||
import { errorFailure, mismatchFailure } from "../shared/failure";
|
||||
import { httpCheckerSchemas } from "./contract";
|
||||
import { checkDuration } from "../../expect/duration";
|
||||
import { errorFailure, mismatchFailure } from "../../expect/failure";
|
||||
import { parseSize } from "../../utils";
|
||||
import { checkBodyExpect } from "./body";
|
||||
import { checkHeaders, checkStatus } from "./expect";
|
||||
import { httpCheckerSchemas } from "./schema";
|
||||
import { validateHttpConfig } from "./validate";
|
||||
|
||||
const CHARSET_RE = /charset="?([^";\s]+)"?/i;
|
||||
@@ -22,7 +23,7 @@ export class HttpChecker implements Checker {
|
||||
|
||||
readonly type = "http";
|
||||
|
||||
async execute(target: ResolvedTarget, ctx: CheckerContext): Promise<CheckResult> {
|
||||
async execute(target: ResolvedTargetBase, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const t = target as ResolvedHttpTarget;
|
||||
const timestamp = new Date().toISOString();
|
||||
const expect = t.expect;
|
||||
@@ -116,15 +117,17 @@ export class HttpChecker implements Checker {
|
||||
}
|
||||
}
|
||||
|
||||
resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget {
|
||||
const t = target as TargetConfig & { http: HttpTargetConfig; type: "http" };
|
||||
const httpDefaults = context.defaults.http;
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTargetBase {
|
||||
const t = target as RawTargetConfig & { http: HttpTargetConfig; type: "http" };
|
||||
const httpDefaults = context.defaults["http"] as
|
||||
| undefined
|
||||
| { headers?: Record<string, string>; maxBodyBytes?: string; method?: string };
|
||||
|
||||
const method = t.http.method ?? httpDefaults?.method ?? "GET";
|
||||
const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? httpDefaults?.maxBodyBytes ?? "100MB");
|
||||
|
||||
return {
|
||||
expect: target.expect,
|
||||
expect: target.expect as HttpExpectConfig | undefined,
|
||||
group: target.group ?? "default",
|
||||
http: {
|
||||
body: t.http.body,
|
||||
@@ -142,7 +145,7 @@ export class HttpChecker implements Checker {
|
||||
} satisfies ResolvedHttpTarget;
|
||||
}
|
||||
|
||||
serialize(target: ResolvedTarget): { config: string; target: string } {
|
||||
serialize(target: ResolvedTargetBase): { config: string; target: string } {
|
||||
const t = target as ResolvedHttpTarget;
|
||||
return {
|
||||
config: JSON.stringify({
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { HeaderExpect } from "../../types";
|
||||
import type { ExpectResult } from "../shared/duration";
|
||||
import type { ExpectResult } from "../../expect/types";
|
||||
import type { HeaderExpect } from "./types";
|
||||
|
||||
import { mismatchFailure } from "../shared/failure";
|
||||
import { applyOperator } from "../shared/operator";
|
||||
import { mismatchFailure } from "../../expect/failure";
|
||||
import { applyOperator } from "../../expect/operator";
|
||||
|
||||
export function checkHeaders(
|
||||
headers: Record<string, string>,
|
||||
|
||||
1
src/server/checker/runner/http/index.ts
Normal file
1
src/server/checker/runner/http/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { HttpChecker } from "./execute";
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
sizeSchema,
|
||||
statusCodePatternSchema,
|
||||
stringMapSchema,
|
||||
} from "../../config-contract/fragments";
|
||||
} from "../../schema/fragments";
|
||||
|
||||
export const httpCheckerSchemas: CheckerSchemas = {
|
||||
config: Type.Object(
|
||||
59
src/server/checker/runner/http/types.ts
Normal file
59
src/server/checker/runner/http/types.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ExpectOperator, ResolvedTargetBase } from "../../types";
|
||||
|
||||
export type BodyRule =
|
||||
| { contains: string }
|
||||
| { css: CssRule }
|
||||
| { json: JsonRule }
|
||||
| { regex: string }
|
||||
| { xpath: XpathRule };
|
||||
|
||||
export type CssRule = ExpectOperator & { attr?: string; selector: string };
|
||||
|
||||
export type HeaderExpect = ExpectOperator | string;
|
||||
|
||||
export interface HttpDefaultsConfig {
|
||||
headers?: Record<string, string>;
|
||||
maxBodyBytes?: string;
|
||||
method?: string;
|
||||
}
|
||||
|
||||
export interface HttpExpectConfig {
|
||||
body?: BodyRule[];
|
||||
headers?: Record<string, HeaderExpect>;
|
||||
maxDurationMs?: number;
|
||||
status?: Array<number | string>;
|
||||
}
|
||||
|
||||
export interface HttpTargetConfig {
|
||||
body?: string;
|
||||
headers?: Record<string, string>;
|
||||
ignoreSSL?: boolean;
|
||||
maxBodyBytes?: string;
|
||||
maxRedirects?: number;
|
||||
method?: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type JsonRule = ExpectOperator & { path: string };
|
||||
|
||||
export interface ResolvedHttpConfig {
|
||||
body?: string;
|
||||
headers: Record<string, string>;
|
||||
ignoreSSL: boolean;
|
||||
maxBodyBytes: number;
|
||||
maxRedirects: number;
|
||||
method: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface ResolvedHttpTarget extends ResolvedTargetBase {
|
||||
expect?: HttpExpectConfig;
|
||||
group: string;
|
||||
http: ResolvedHttpConfig;
|
||||
intervalMs: number;
|
||||
name: string;
|
||||
timeoutMs: number;
|
||||
type: "http";
|
||||
}
|
||||
|
||||
export type XpathRule = ExpectOperator & { path: string };
|
||||
@@ -1,15 +1,26 @@
|
||||
import type { ConfigValidationIssue } from "../../config-contract/issues";
|
||||
import { DOMParser } from "@xmldom/xmldom";
|
||||
import * as xpath from "xpath";
|
||||
|
||||
import type { ConfigValidationIssue } from "../../schema/issues";
|
||||
import type { CheckerValidationInput } from "../types";
|
||||
|
||||
import { issue, joinPath } from "../../config-contract/issues";
|
||||
import { parseSize } from "../../size";
|
||||
import { validateBodyRules, validateOperatorObject } from "../shared/validate";
|
||||
import { isPlainRecord, validateOperatorObject } from "../../expect/validate-operator";
|
||||
import { BodyRuleTypeKeys, OperatorKeys } from "../../schema/fragments";
|
||||
import { issue, joinPath } from "../../schema/issues";
|
||||
import { parseSize } from "../../utils";
|
||||
|
||||
const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);
|
||||
const OPERATOR_KEY_SET = new Set<string>(OperatorKeys);
|
||||
|
||||
export function validateBodyRules(body: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!Array.isArray(body)) return [issue("invalid-type", path, "必须为数组", targetName)];
|
||||
return body.flatMap((rule, index) => validateSingleBodyRule(rule, `${path}[${index}]`, targetName));
|
||||
}
|
||||
|
||||
export function validateHttpConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const defaults = isRecord(input.defaults) && isRecord(input.defaults["http"]) ? input.defaults["http"] : undefined;
|
||||
const defaults =
|
||||
isPlainRecord(input.defaults) && isPlainRecord(input.defaults["http"]) ? input.defaults["http"] : undefined;
|
||||
|
||||
if (isSizeInput(defaults?.["maxBodyBytes"])) {
|
||||
issues.push(...validateSizeValue(defaults["maxBodyBytes"], "defaults.http.maxBodyBytes"));
|
||||
@@ -17,7 +28,7 @@ export function validateHttpConfig(input: CheckerValidationInput): ConfigValidat
|
||||
|
||||
for (let i = 0; i < input.targets.length; i++) {
|
||||
const target = input.targets[i] as unknown;
|
||||
if (!isRecord(target)) continue;
|
||||
if (!isPlainRecord(target)) continue;
|
||||
if (target["type"] !== "http") continue;
|
||||
issues.push(...validateHttpTarget(target, `targets[${i}]`));
|
||||
}
|
||||
@@ -25,6 +36,43 @@ export function validateHttpConfig(input: CheckerValidationInput): ConfigValidat
|
||||
return issues;
|
||||
}
|
||||
|
||||
export function validateJsonPath(path: string, rulePath: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!path.startsWith("$.") || path.length <= 2) {
|
||||
return [issue("invalid-jsonpath", joinPath(rulePath, "path"), '必须为以 "$." 开头的有效 JSONPath', targetName)];
|
||||
}
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const segments = path.slice(2).split(".");
|
||||
for (const seg of segments) {
|
||||
if (seg === "") {
|
||||
issues.push(issue("invalid-jsonpath", joinPath(rulePath, "path"), "包含空段", targetName));
|
||||
}
|
||||
const bracketMatch = /^(.+?)\[(\d+)\]$/.exec(seg);
|
||||
if (bracketMatch?.[1]!.trim() === "") {
|
||||
issues.push(issue("invalid-jsonpath", joinPath(rulePath, "path"), "数组访问缺少属性名", targetName));
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
function collectOperatorObject(
|
||||
object: Record<string, unknown>,
|
||||
allowedKeys: Set<string>,
|
||||
path: string,
|
||||
targetName?: string,
|
||||
): { issues: ConfigValidationIssue[]; operators: Record<string, unknown> } {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const operators: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
if (allowedKeys.has(key)) continue;
|
||||
if (OPERATOR_KEY_SET.has(key)) {
|
||||
operators[key] = value;
|
||||
} else {
|
||||
issues.push(issue("unknown-field", joinPath(path, key), "是未知字段", targetName));
|
||||
}
|
||||
}
|
||||
return { issues, operators };
|
||||
}
|
||||
|
||||
function getTargetName(target: Record<string, unknown>): string | undefined {
|
||||
return typeof target["name"] === "string" ? target["name"] : undefined;
|
||||
}
|
||||
@@ -33,22 +81,35 @@ function isNonNegativeFiniteNumber(value: unknown): boolean {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isSizeInput(value: unknown): value is number | string {
|
||||
return typeof value === "number" || typeof value === "string";
|
||||
}
|
||||
|
||||
function validateCssRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!isPlainRecord(rule)) return [issue("invalid-type", path, "必须为对象", targetName)];
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
if (typeof rule["selector"] !== "string" || rule["selector"].trim() === "") {
|
||||
issues.push(issue("invalid-type", joinPath(path, "selector"), "必须为非空字符串", targetName));
|
||||
}
|
||||
if ("attr" in rule && typeof rule["attr"] !== "string") {
|
||||
issues.push(issue("invalid-type", joinPath(path, "attr"), "必须为字符串", targetName));
|
||||
}
|
||||
const result = collectOperatorObject(rule, new Set(["attr", "selector"]), path, targetName);
|
||||
issues.push(
|
||||
...result.issues,
|
||||
...validateOperatorObject(result.operators, path, targetName, { requireAtLeastOne: false }),
|
||||
);
|
||||
return issues;
|
||||
}
|
||||
|
||||
function validateHttpExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||
const targetName = getTargetName(target);
|
||||
const expect = target["expect"];
|
||||
if (expect === undefined || expect === null || !isRecord(expect)) return [];
|
||||
if (expect === undefined || expect === null || !isPlainRecord(expect)) return [];
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const expectPath = joinPath(path, "expect");
|
||||
|
||||
if (isRecord(expect["headers"])) {
|
||||
if (isPlainRecord(expect["headers"])) {
|
||||
for (const [key, value] of Object.entries(expect["headers"])) {
|
||||
if (typeof value === "string") continue;
|
||||
issues.push(...validateOperatorObject(value, joinPath(joinPath(expectPath, "headers"), key), targetName));
|
||||
@@ -74,7 +135,7 @@ function validateHttpTarget(target: Record<string, unknown>, path: string): Conf
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const targetName = getTargetName(target);
|
||||
const http = target["http"];
|
||||
if (!isRecord(http)) {
|
||||
if (!isPlainRecord(http)) {
|
||||
issues.push(issue("required", joinPath(path, "http"), "缺少 http.url 字段", targetName));
|
||||
issues.push(...validateHttpExpect(target, path));
|
||||
return issues;
|
||||
@@ -107,6 +168,61 @@ function validateHttpTarget(target: Record<string, unknown>, path: string): Conf
|
||||
return issues;
|
||||
}
|
||||
|
||||
function validateJsonRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!isPlainRecord(rule)) return [issue("invalid-type", path, "必须为对象", targetName)];
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
if (typeof rule["path"] !== "string") {
|
||||
issues.push(issue("invalid-type", joinPath(path, "path"), "必须为字符串", targetName));
|
||||
} else {
|
||||
issues.push(...validateJsonPath(rule["path"], path, targetName));
|
||||
}
|
||||
const result = collectOperatorObject(rule, new Set(["path"]), path, targetName);
|
||||
issues.push(
|
||||
...result.issues,
|
||||
...validateOperatorObject(result.operators, path, targetName, { requireAtLeastOne: false }),
|
||||
);
|
||||
return issues;
|
||||
}
|
||||
|
||||
function validateRegexRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (typeof rule !== "string") return [issue("invalid-type", path, "必须为字符串", targetName)];
|
||||
try {
|
||||
new RegExp(rule);
|
||||
return [];
|
||||
} catch {
|
||||
return [issue("invalid-regex", path, "正则不合法", targetName)];
|
||||
}
|
||||
}
|
||||
|
||||
function validateSingleBodyRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!isPlainRecord(rule)) return [issue("invalid-type", path, "必须为对象", targetName)];
|
||||
const found = BodyRuleTypeKeys.filter((type) => type in rule);
|
||||
if (found.length === 0) return [issue("missing-body-rule", path, "缺少支持的规则类型", targetName)];
|
||||
if (found.length > 1) return [issue("multiple-body-rules", path, "只能配置一种规则类型", targetName)];
|
||||
|
||||
const ruleType = found[0]!;
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
for (const key of Object.keys(rule)) {
|
||||
if (key !== ruleType) issues.push(issue("unknown-field", joinPath(path, key), "是未知字段", targetName));
|
||||
}
|
||||
if (issues.length > 0) return issues;
|
||||
|
||||
switch (ruleType) {
|
||||
case "contains":
|
||||
return typeof rule["contains"] === "string"
|
||||
? []
|
||||
: [issue("invalid-type", joinPath(path, "contains"), "必须为字符串", targetName)];
|
||||
case "css":
|
||||
return validateCssRule(rule["css"], joinPath(path, "css"), targetName);
|
||||
case "json":
|
||||
return validateJsonRule(rule["json"], joinPath(path, "json"), targetName);
|
||||
case "regex":
|
||||
return validateRegexRule(rule["regex"], joinPath(path, "regex"), targetName);
|
||||
case "xpath":
|
||||
return validateXpathRule(rule["xpath"], joinPath(path, "xpath"), targetName);
|
||||
}
|
||||
}
|
||||
|
||||
function validateSizeValue(value: number | string, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
try {
|
||||
parseSize(value);
|
||||
@@ -137,3 +253,24 @@ function validateStatusValues(values: unknown[], path: string, targetName?: stri
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
function validateXpathRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!isPlainRecord(rule)) return [issue("invalid-type", path, "必须为对象", targetName)];
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
if (typeof rule["path"] !== "string" || rule["path"].trim() === "") {
|
||||
issues.push(issue("invalid-type", joinPath(path, "path"), "必须为非空字符串", targetName));
|
||||
} else {
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString("<x/>", "text/xml");
|
||||
xpath.select(rule["path"], doc as unknown as Node);
|
||||
} catch {
|
||||
issues.push(issue("invalid-xpath", joinPath(path, "path"), "xpath 不合法", targetName));
|
||||
}
|
||||
}
|
||||
const result = collectOperatorObject(rule, new Set(["path"]), path, targetName);
|
||||
issues.push(
|
||||
...result.issues,
|
||||
...validateOperatorObject(result.operators, path, targetName, { requireAtLeastOne: false }),
|
||||
);
|
||||
return issues;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { CommandChecker } from "./command/runner";
|
||||
import { HttpChecker } from "./http/runner";
|
||||
import { CheckerRegistry, checkerRegistry } from "./registry";
|
||||
import { CommandChecker } from "./command";
|
||||
import { HttpChecker } from "./http";
|
||||
import { CheckerRegistry } from "./registry";
|
||||
|
||||
const checkers = [new HttpChecker(), new CommandChecker()];
|
||||
|
||||
export function createDefaultCheckerRegistry(): CheckerRegistry {
|
||||
const registry = new CheckerRegistry();
|
||||
registerCheckers(registry);
|
||||
for (const checker of checkers) {
|
||||
registry.register(checker);
|
||||
}
|
||||
return registry;
|
||||
}
|
||||
|
||||
export function registerCheckers(registry = checkerRegistry): void {
|
||||
registry.register(new HttpChecker());
|
||||
registry.register(new CommandChecker());
|
||||
}
|
||||
|
||||
export { checkerRegistry } from "./registry";
|
||||
export const checkerRegistry = createDefaultCheckerRegistry();
|
||||
|
||||
@@ -30,5 +30,3 @@ export class CheckerRegistry {
|
||||
return this.checkers.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
export const checkerRegistry = new CheckerRegistry();
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { CheckFailure } from "../../types";
|
||||
|
||||
import { mismatchFailure } from "./failure";
|
||||
|
||||
export interface ExpectResult {
|
||||
failure: CheckFailure | null;
|
||||
matched: boolean;
|
||||
}
|
||||
|
||||
export function checkDuration(durationMs: number, maxDurationMs?: number): ExpectResult {
|
||||
if (maxDurationMs === undefined) return { failure: null, matched: true };
|
||||
if (durationMs > maxDurationMs) {
|
||||
return {
|
||||
failure: mismatchFailure(
|
||||
"duration",
|
||||
"duration",
|
||||
`<=${maxDurationMs}ms`,
|
||||
durationMs,
|
||||
`duration ${durationMs}ms > ${maxDurationMs}ms`,
|
||||
),
|
||||
matched: false,
|
||||
};
|
||||
}
|
||||
return { failure: null, matched: true };
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { CheckFailure } from "../../types";
|
||||
|
||||
export function errorFailure(phase: CheckFailure["phase"], path: string, message: string): CheckFailure {
|
||||
return {
|
||||
kind: "error",
|
||||
message,
|
||||
path,
|
||||
phase,
|
||||
};
|
||||
}
|
||||
|
||||
export function mismatchFailure(
|
||||
phase: CheckFailure["phase"],
|
||||
path: string,
|
||||
expected: unknown,
|
||||
actual: unknown,
|
||||
message: string,
|
||||
): CheckFailure {
|
||||
return {
|
||||
actual: truncateActual(actual),
|
||||
expected,
|
||||
kind: "mismatch",
|
||||
message,
|
||||
path,
|
||||
phase,
|
||||
};
|
||||
}
|
||||
|
||||
export function truncateActual(value: unknown, maxLen = 200): unknown {
|
||||
if (value === undefined || value === null) return value;
|
||||
const str = typeof value === "string" ? value : JSON.stringify(value);
|
||||
if (str.length <= maxLen) return value;
|
||||
return str.slice(0, maxLen) + "...";
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { isEmptyObject, isEqual, isNil, isPlainObject } from "es-toolkit";
|
||||
|
||||
import type { ExpectOperator, ExpectValue } from "../../types";
|
||||
|
||||
const OPERATOR_KEYS = new Set(["contains", "empty", "equals", "exists", "gt", "gte", "lt", "lte", "match"]);
|
||||
|
||||
export function applyOperator(actual: unknown, op: ExpectOperator): boolean {
|
||||
for (const [key, expected] of Object.entries(op)) {
|
||||
if (expected === undefined) continue;
|
||||
|
||||
switch (key) {
|
||||
case "contains":
|
||||
if (!String(actual).includes(expected as string)) return false;
|
||||
break;
|
||||
case "empty": {
|
||||
const isEmpty =
|
||||
isNil(actual) || actual === "" || (Array.isArray(actual) && actual.length === 0) || isEmptyObject(actual);
|
||||
if (expected !== isEmpty) return false;
|
||||
break;
|
||||
}
|
||||
case "equals":
|
||||
if (!isEqual(actual, expected)) return false;
|
||||
break;
|
||||
case "exists":
|
||||
if (expected) {
|
||||
if (actual === undefined) return false;
|
||||
} else {
|
||||
if (actual !== undefined) return false;
|
||||
}
|
||||
break;
|
||||
case "gt":
|
||||
if (!(Number(actual) > (expected as number))) return false;
|
||||
break;
|
||||
case "gte":
|
||||
if (!(Number(actual) >= (expected as number))) return false;
|
||||
break;
|
||||
case "lt":
|
||||
if (!(Number(actual) < (expected as number))) return false;
|
||||
break;
|
||||
case "lte":
|
||||
if (!(Number(actual) <= (expected as number))) return false;
|
||||
break;
|
||||
case "match":
|
||||
if (!new RegExp(expected as string).test(String(actual))) return false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function checkExpectValue(actual: unknown, expected: ExpectValue): boolean {
|
||||
if (isPlainObject(expected) && Object.keys(expected).some((key) => OPERATOR_KEYS.has(key))) {
|
||||
return applyOperator(actual, expected as ExpectOperator);
|
||||
}
|
||||
return applyOperator(actual, { equals: expected as Exclude<ExpectValue, ExpectOperator> });
|
||||
}
|
||||
|
||||
export function evaluateJsonPath(json: unknown, path: string): unknown {
|
||||
if (!path.startsWith("$.")) return undefined;
|
||||
|
||||
const segments = path.slice(2).split(".");
|
||||
let current: unknown = json;
|
||||
|
||||
for (const seg of segments) {
|
||||
const bracketMatch = /^(.+?)\[(\d+)\]$/.exec(seg);
|
||||
if (bracketMatch) {
|
||||
current = (current as Record<string, unknown>)?.[bracketMatch[1]!];
|
||||
const idx = parseInt(bracketMatch[2]!, 10);
|
||||
if (!Array.isArray(current) || idx >= current.length) return undefined;
|
||||
current = current[idx];
|
||||
} else {
|
||||
if (current === null || current === undefined) return undefined;
|
||||
current = (current as Record<string, unknown>)[seg];
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import { DOMParser } from "@xmldom/xmldom";
|
||||
import * as xpath from "xpath";
|
||||
|
||||
import type { ConfigValidationIssue } from "../../config-contract/issues";
|
||||
import type { JsonValue } from "../../types";
|
||||
|
||||
import { BodyRuleTypeKeys, OperatorKeys } from "../../config-contract/fragments";
|
||||
import { issue, joinPath } from "../../config-contract/issues";
|
||||
|
||||
const OPERATOR_KEY_SET = new Set<string>(OperatorKeys);
|
||||
|
||||
export function isJsonValue(value: unknown): value is JsonValue {
|
||||
if (value === null) return true;
|
||||
if (typeof value === "string" || typeof value === "boolean") return true;
|
||||
if (typeof value === "number") return Number.isFinite(value);
|
||||
if (Array.isArray(value)) return value.every(isJsonValue);
|
||||
if (typeof value === "object") {
|
||||
return Object.values(value as Record<string, unknown>).every(isJsonValue);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function validateBodyRules(body: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!Array.isArray(body)) return [issue("invalid-type", path, "必须为数组", targetName)];
|
||||
return body.flatMap((rule, index) => validateSingleBodyRule(rule, `${path}[${index}]`, targetName));
|
||||
}
|
||||
|
||||
export function validateJsonPath(path: string, rulePath: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!path.startsWith("$.") || path.length <= 2) {
|
||||
return [issue("invalid-jsonpath", joinPath(rulePath, "path"), '必须为以 "$." 开头的有效 JSONPath', targetName)];
|
||||
}
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const segments = path.slice(2).split(".");
|
||||
for (const seg of segments) {
|
||||
if (seg === "") {
|
||||
issues.push(issue("invalid-jsonpath", joinPath(rulePath, "path"), "包含空段", targetName));
|
||||
}
|
||||
const bracketMatch = /^(.+?)\[(\d+)\]$/.exec(seg);
|
||||
if (bracketMatch?.[1]!.trim() === "") {
|
||||
issues.push(issue("invalid-jsonpath", joinPath(rulePath, "path"), "数组访问缺少属性名", targetName));
|
||||
}
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
export function validateOperatorObject(
|
||||
operators: unknown,
|
||||
path: string,
|
||||
targetName?: string,
|
||||
options: { requireAtLeastOne: boolean } = { requireAtLeastOne: true },
|
||||
): ConfigValidationIssue[] {
|
||||
if (!isPlainRecord(operators)) return [issue("invalid-type", path, "必须为操作符对象", targetName)];
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
let found = 0;
|
||||
for (const [key, value] of Object.entries(operators)) {
|
||||
if (!OPERATOR_KEY_SET.has(key)) {
|
||||
issues.push(issue("unknown-operator", joinPath(path, key), "是未知 operator", targetName));
|
||||
continue;
|
||||
}
|
||||
if (value === undefined) continue;
|
||||
found++;
|
||||
issues.push(...validateOperatorValue(key, value, joinPath(path, key), targetName));
|
||||
}
|
||||
if (options.requireAtLeastOne && found === 0) {
|
||||
issues.push(issue("empty-operator", path, "必须包含至少一个合法 operator", targetName));
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
|
||||
export function validateTextRules(rules: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!Array.isArray(rules)) return [issue("invalid-type", path, "必须为数组", targetName)];
|
||||
return rules.flatMap((rule, index) => validateOperatorObject(rule, `${path}[${index}]`, targetName));
|
||||
}
|
||||
|
||||
function collectOperatorObject(
|
||||
object: Record<string, unknown>,
|
||||
allowedKeys: Set<string>,
|
||||
path: string,
|
||||
targetName?: string,
|
||||
): { issues: ConfigValidationIssue[]; operators: Record<string, unknown> } {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const operators: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
if (allowedKeys.has(key)) continue;
|
||||
if (OPERATOR_KEY_SET.has(key)) {
|
||||
operators[key] = value;
|
||||
} else {
|
||||
issues.push(issue("unknown-field", joinPath(path, key), "是未知字段", targetName));
|
||||
}
|
||||
}
|
||||
return { issues, operators };
|
||||
}
|
||||
|
||||
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function validateCssRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!isPlainRecord(rule)) return [issue("invalid-type", path, "必须为对象", targetName)];
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
if (typeof rule["selector"] !== "string" || rule["selector"].trim() === "") {
|
||||
issues.push(issue("invalid-type", joinPath(path, "selector"), "必须为非空字符串", targetName));
|
||||
}
|
||||
if ("attr" in rule && typeof rule["attr"] !== "string") {
|
||||
issues.push(issue("invalid-type", joinPath(path, "attr"), "必须为字符串", targetName));
|
||||
}
|
||||
const result = collectOperatorObject(rule, new Set(["attr", "selector"]), path, targetName);
|
||||
issues.push(
|
||||
...result.issues,
|
||||
...validateOperatorObject(result.operators, path, targetName, { requireAtLeastOne: false }),
|
||||
);
|
||||
return issues;
|
||||
}
|
||||
|
||||
function validateJsonRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!isPlainRecord(rule)) return [issue("invalid-type", path, "必须为对象", targetName)];
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
if (typeof rule["path"] !== "string") {
|
||||
issues.push(issue("invalid-type", joinPath(path, "path"), "必须为字符串", targetName));
|
||||
} else {
|
||||
issues.push(...validateJsonPath(rule["path"], path, targetName));
|
||||
}
|
||||
const result = collectOperatorObject(rule, new Set(["path"]), path, targetName);
|
||||
issues.push(
|
||||
...result.issues,
|
||||
...validateOperatorObject(result.operators, path, targetName, { requireAtLeastOne: false }),
|
||||
);
|
||||
return issues;
|
||||
}
|
||||
|
||||
function validateOperatorValue(
|
||||
key: string,
|
||||
value: unknown,
|
||||
path: string,
|
||||
targetName?: string,
|
||||
): ConfigValidationIssue[] {
|
||||
switch (key) {
|
||||
case "contains":
|
||||
return typeof value === "string" ? [] : [issue("invalid-type", path, "必须为字符串", targetName)];
|
||||
case "empty":
|
||||
case "exists":
|
||||
return typeof value === "boolean" ? [] : [issue("invalid-type", path, "必须为布尔值", targetName)];
|
||||
case "equals":
|
||||
return isJsonValue(value) ? [] : [issue("invalid-type", path, "必须为 JSON value", targetName)];
|
||||
case "gt":
|
||||
case "gte":
|
||||
case "lt":
|
||||
case "lte":
|
||||
return typeof value === "number" && Number.isFinite(value)
|
||||
? []
|
||||
: [issue("invalid-type", path, "必须为有限数字", targetName)];
|
||||
case "match":
|
||||
if (typeof value !== "string") return [issue("invalid-type", path, "必须为字符串", targetName)];
|
||||
try {
|
||||
new RegExp(value);
|
||||
return [];
|
||||
} catch {
|
||||
return [issue("invalid-regex", path, "正则不合法", targetName)];
|
||||
}
|
||||
default:
|
||||
return [issue("unknown-operator", path, "是未知 operator", targetName)];
|
||||
}
|
||||
}
|
||||
|
||||
function validateRegexRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (typeof rule !== "string") return [issue("invalid-type", path, "必须为字符串", targetName)];
|
||||
try {
|
||||
new RegExp(rule);
|
||||
return [];
|
||||
} catch {
|
||||
return [issue("invalid-regex", path, "正则不合法", targetName)];
|
||||
}
|
||||
}
|
||||
|
||||
function validateSingleBodyRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!isPlainRecord(rule)) return [issue("invalid-type", path, "必须为对象", targetName)];
|
||||
const found = BodyRuleTypeKeys.filter((type) => type in rule);
|
||||
if (found.length === 0) return [issue("missing-body-rule", path, "缺少支持的规则类型", targetName)];
|
||||
if (found.length > 1) return [issue("multiple-body-rules", path, "只能配置一种规则类型", targetName)];
|
||||
|
||||
const ruleType = found[0]!;
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
for (const key of Object.keys(rule)) {
|
||||
if (key !== ruleType) issues.push(issue("unknown-field", joinPath(path, key), "是未知字段", targetName));
|
||||
}
|
||||
if (issues.length > 0) return issues;
|
||||
|
||||
switch (ruleType) {
|
||||
case "contains":
|
||||
return typeof rule["contains"] === "string"
|
||||
? []
|
||||
: [issue("invalid-type", joinPath(path, "contains"), "必须为字符串", targetName)];
|
||||
case "css":
|
||||
return validateCssRule(rule["css"], joinPath(path, "css"), targetName);
|
||||
case "json":
|
||||
return validateJsonRule(rule["json"], joinPath(path, "json"), targetName);
|
||||
case "regex":
|
||||
return validateRegexRule(rule["regex"], joinPath(path, "regex"), targetName);
|
||||
case "xpath":
|
||||
return validateXpathRule(rule["xpath"], joinPath(path, "xpath"), targetName);
|
||||
}
|
||||
}
|
||||
|
||||
function validateXpathRule(rule: unknown, path: string, targetName?: string): ConfigValidationIssue[] {
|
||||
if (!isPlainRecord(rule)) return [issue("invalid-type", path, "必须为对象", targetName)];
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
if (typeof rule["path"] !== "string" || rule["path"].trim() === "") {
|
||||
issues.push(issue("invalid-type", joinPath(path, "path"), "必须为非空字符串", targetName));
|
||||
} else {
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString("<x/>", "text/xml");
|
||||
xpath.select(rule["path"], doc as unknown as Node);
|
||||
} catch {
|
||||
issues.push(issue("invalid-xpath", joinPath(path, "path"), "xpath 不合法", targetName));
|
||||
}
|
||||
}
|
||||
const result = collectOperatorObject(rule, new Set(["path"]), path, targetName);
|
||||
issues.push(
|
||||
...result.issues,
|
||||
...validateOperatorObject(result.operators, path, targetName, { requireAtLeastOne: false }),
|
||||
);
|
||||
return issues;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { TSchema } from "@sinclair/typebox";
|
||||
|
||||
import type { ConfigValidationIssue } from "../config-contract/issues";
|
||||
import type { CheckResult, DefaultsConfig, ResolvedTarget, TargetConfig } from "../types";
|
||||
import type { ConfigValidationIssue } from "../schema/issues";
|
||||
import type { CheckResult, DefaultsConfig, RawTargetConfig, ResolvedTargetBase } from "../types";
|
||||
|
||||
export type Checker = CheckerDefinition;
|
||||
|
||||
@@ -11,10 +11,10 @@ export interface CheckerContext {
|
||||
|
||||
export interface CheckerDefinition {
|
||||
readonly configKey: string;
|
||||
execute(target: ResolvedTarget, ctx: CheckerContext): Promise<CheckResult>;
|
||||
resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget;
|
||||
execute(target: ResolvedTargetBase, ctx: CheckerContext): Promise<CheckResult>;
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedTargetBase;
|
||||
readonly schemas: CheckerSchemas;
|
||||
serialize(target: ResolvedTarget): { config: string; target: string };
|
||||
serialize(target: ResolvedTargetBase): { config: string; target: string };
|
||||
readonly type: string;
|
||||
validate(input: CheckerValidationInput): ConfigValidationIssue[];
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export interface CheckerSchemas {
|
||||
|
||||
export interface CheckerValidationInput {
|
||||
defaults: DefaultsConfig;
|
||||
targets: TargetConfig[];
|
||||
targets: RawTargetConfig[];
|
||||
}
|
||||
|
||||
export interface ResolveContext {
|
||||
|
||||
Reference in New Issue
Block a user