From a5cf6065c2f4fd3961d7537127a5e7b86c1fdd40 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Tue, 12 May 2026 18:44:59 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=BC=BA=E5=8C=96=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E8=B4=A8=E9=87=8F=E4=B8=8E=E9=A3=8E=E6=A0=BC=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E4=BD=93=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESLint 升级到 recommended-type-checked + stylistic-type-checked, 引入 perfectionist 导入排序和 import 插件导入验证。 Prettier 显式声明全部格式化参数,消除跨环境差异。 TypeScript 启用 noUnusedLocals 和 noPropertyAccessFromIndexSignature。 完善 ignore 列表,排除 .agents/、bun.lock、data/ 等。 引入 husky + lint-staged(pre-commit)+ commitlint(commit-msg)。 更新 DEVELOPMENT.md 代码质量章节。 修复所有新增规则检测到的类型和风格违规。 --- .husky/commit-msg | 1 + .husky/pre-commit | 1 + .lintstagedrc.json | 4 + .prettierignore | 3 + .prettierrc.json | 10 +- DEVELOPMENT.md | 171 +++--- bun.lock | 491 +++++++++++++++++- commitlint.config.js | 8 + eslint.config.js | 39 +- openspec/specs/code-quality-gates/spec.md | 66 ++- openspec/specs/commit-quality-gates/spec.md | 50 ++ package.json | 10 +- scripts/build.ts | 38 +- scripts/clean.ts | 10 +- scripts/dev.ts | 8 +- scripts/smoke.ts | 105 ++-- src/server/app.ts | 23 +- src/server/checker/config-loader.ts | 56 +- src/server/checker/engine.ts | 38 +- src/server/checker/runner/command/expect.ts | 9 +- src/server/checker/runner/command/runner.ts | 415 +++++++-------- src/server/checker/runner/http/expect.ts | 55 +- src/server/checker/runner/http/runner.ts | 125 ++--- src/server/checker/runner/index.ts | 4 +- src/server/checker/runner/registry.ts | 18 +- src/server/checker/runner/shared/body.ts | 178 +++---- src/server/checker/runner/shared/duration.ts | 9 +- src/server/checker/runner/shared/failure.ts | 32 +- src/server/checker/runner/shared/operator.ts | 107 ++-- src/server/checker/runner/shared/text.ts | 9 +- src/server/checker/runner/types.ts | 19 +- src/server/checker/size.ts | 2 +- src/server/checker/store.ts | 422 +++++++-------- src/server/checker/types.ts | 274 +++++----- src/server/config.ts | 10 +- src/server/dev.ts | 6 +- src/server/helpers.ts | 58 +-- src/server/middleware.ts | 57 +- src/server/routes/health.ts | 3 +- src/server/routes/history.ts | 7 +- src/server/routes/summary.ts | 5 +- src/server/routes/targets.ts | 15 +- src/server/routes/trend.ts | 5 +- src/server/server.ts | 7 +- src/server/static.ts | 79 +-- src/shared/api.ts | 92 ++-- src/web/app.tsx | 41 +- src/web/components/GroupHeader.tsx | 10 +- src/web/components/StatusBar.tsx | 4 +- src/web/components/StatusDonut.tsx | 12 +- src/web/components/SummaryCards.tsx | 15 +- src/web/components/TargetBoard.tsx | 10 +- src/web/components/TargetDetailDrawer.tsx | 161 +++--- src/web/components/TargetGroup.tsx | 30 +- src/web/components/TrendChart.tsx | 39 +- src/web/constants/target-table-columns.tsx | 73 +-- src/web/constants/target-table-filters.ts | 4 +- src/web/constants/target-table-sorters.ts | 18 +- src/web/constants/target-type-display.ts | 2 +- src/web/hooks/useTargetDetail.ts | 76 +-- src/web/main.tsx | 8 +- tests/server/app.test.ts | 142 ++--- tests/server/checker/config-loader.test.ts | 42 +- tests/server/checker/engine.test.ts | 100 ++-- .../checker/runner/command/expect.test.ts | 1 + .../checker/runner/command/runner.test.ts | 92 ++-- .../server/checker/runner/http/expect.test.ts | 37 +- .../server/checker/runner/http/runner.test.ts | 109 ++-- tests/server/checker/runner/registry.test.ts | 11 +- .../server/checker/runner/shared/body.test.ts | 49 +- .../checker/runner/shared/duration.test.ts | 1 + .../checker/runner/shared/failure.test.ts | 15 +- .../checker/runner/shared/operator.test.ts | 15 +- .../server/checker/runner/shared/text.test.ts | 7 +- tests/server/checker/size.test.ts | 1 + tests/server/checker/store.test.ts | 119 +++-- tests/server/config.test.ts | 1 + tests/web/constants/color-threshold.test.ts | 3 +- .../constants/target-table-filters.test.ts | 3 +- .../constants/target-table-sorters.test.ts | 46 +- .../web/constants/target-type-display.test.ts | 5 +- tsconfig.json | 4 +- vite.config.ts | 18 +- 83 files changed, 2654 insertions(+), 1824 deletions(-) create mode 100755 .husky/commit-msg create mode 100755 .husky/pre-commit create mode 100644 .lintstagedrc.json create mode 100644 commitlint.config.js create mode 100644 openspec/specs/commit-quality-gates/spec.md diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..3b9e0aa --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +bunx commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..ea5a55b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +bunx lint-staged diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000..76f7ce9 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,4 @@ +{ + "*.{ts,tsx}": ["eslint --fix", "prettier --write"], + "*.{md,json,yaml,yml}": ["prettier --write"] +} diff --git a/.prettierignore b/.prettierignore index 5b3382f..2226868 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,6 @@ bun.lock .opencode/ .claude/ .codex/ +.agents/ +skills-lock.json +data/ diff --git a/.prettierrc.json b/.prettierrc.json index 963354f..c785b06 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,3 +1,11 @@ { - "printWidth": 120 + "printWidth": 120, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "tabWidth": 2, + "useTabs": false } diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 05ba56a..69434dd 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -226,15 +226,15 @@ runCommandCheck → 收集观测(exitCode/stdout/stderr/durationMs) ### 2.1 技术栈概览 -| 层面 | 技术 | 用途 | -| -------- | ----------------------------------- | ------------------------------ | -| 框架 | React 19 | UI 组件开发 | -| 构建 | Vite 8 | 开发服务与生产构建 | -| 语言 | TypeScript 6 | 类型安全 | -| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 | -| 数据层 | TanStack Query (React Query) | 服务端状态管理与自动轮询 | -| 图表 | Recharts | 拨测趋势折线图与状态环状图 | -| 路由 | 无(单页面 Dashboard) | 仅需 Drawer/Tab 做页面内导航 | +| 层面 | 技术 | 用途 | +| ------ | ----------------------------------- | ---------------------------- | +| 框架 | React 19 | UI 组件开发 | +| 构建 | Vite 8 | 开发服务与生产构建 | +| 语言 | TypeScript 6 | 类型安全 | +| UI 库 | TDesign React + tdesign-icons-react | UI 组件与图标 | +| 数据层 | TanStack Query (React Query) | 服务端状态管理与自动轮询 | +| 图表 | Recharts | 拨测趋势折线图与状态环状图 | +| 路由 | 无(单页面 Dashboard) | 仅需 Drawer/Tab 做页面内导航 | **不引入的依赖**:React Router(单页面场景不需要)、状态管理库(TanStack Query 即服务端状态层,组件内用 `useState` 足够) @@ -293,7 +293,7 @@ const queryKeys = { useQuery({ queryKey: queryKeys.summary(), queryFn: () => fetchJson("/api/summary"), - refetchInterval: 8000, // 自动轮询间隔 + refetchInterval: 8000, // 自动轮询间隔 refetchIntervalInBackground: false, // 切后台不轮询 }); @@ -324,9 +324,9 @@ async function fetchJson(url: string): Promise { new QueryClient({ defaultOptions: { queries: { - retry: 1, // 失败重试 1 次 + retry: 1, // 失败重试 1 次 refetchOnWindowFocus: true, // 窗口聚焦时刷新 - staleTime: 5000, // 5s 内视为 fresh,避免重复请求 + staleTime: 5000, // 5s 内视为 fresh,避免重复请求 }, }, }); @@ -364,18 +364,18 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps) #### 现有组件清单 -| 组件 | 文件 | 用途 | -| ----------------------- | -------------------------- | ---------------------------- | -| `App` | `app.tsx` | 根组件,编排全局状态与布局 | -| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(全部/正常/异常) | -| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表 | -| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组标题 + PrimaryTable | -| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(概览/记录 Tab) | -| `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) | -| `StatusDonut` | `components/StatusDonut.tsx` | Recharts 环状图(UP/DOWN 分布) | -| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) | -| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块) | -| `GroupHeader` | `components/GroupHeader.tsx` | 分组标题(名称 + 统计) | +| 组件 | 文件 | 用途 | +| -------------------- | ----------------------------------- | ---------------------------------- | +| `App` | `app.tsx` | 根组件,编排全局状态与布局 | +| `SummaryCards` | `components/SummaryCards.tsx` | 总览统计卡片(全部/正常/异常) | +| `TargetBoard` | `components/TargetBoard.tsx` | 按分组渲染目标表格列表 | +| `TargetGroup` | `components/TargetGroup.tsx` | 单个分组标题 + PrimaryTable | +| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉(概览/记录 Tab) | +| `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) | +| `StatusDonut` | `components/StatusDonut.tsx` | Recharts 环状图(UP/DOWN 分布) | +| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) | +| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块) | +| `GroupHeader` | `components/GroupHeader.tsx` | 分组标题(名称 + 统计) | ### 2.5 新增功能开发步骤 @@ -491,6 +491,7 @@ server: { ``` SPA fallback 逻辑(`src/server/static.ts`): + - `/` → index.html - 匹配 `/assets/*` → 返回对应文件(未匹配则 404) - 其他路径(如 `/dashboard`)→ fallback 到 index.html(SPA 路由) @@ -525,16 +526,16 @@ bun run build #### 产物 -| 产物 | 用途 | -| ---------------------------- | ------------------------ | -| `dist/dial-server` | 生产可执行文件 | -| `dist/web/` | Vite 构建产物(中间产物) | -| `.build/` | 临时生成文件(构建后清理) | +| 产物 | 用途 | +| ------------------ | -------------------------- | +| `dist/dial-server` | 生产可执行文件 | +| `dist/web/` | Vite 构建产物(中间产物) | +| `.build/` | 临时生成文件(构建后清理) | #### 构建参数 -| 环境变量 | 说明 | -| ------------------- | ----------------------------------------- | +| 环境变量 | 说明 | +| --------------------------- | -------------------------------------- | | `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(如 `bun-linux-x64`) | #### 运行可执行文件 @@ -587,32 +588,32 @@ bun run test:smoke ### 3.6 脚本说明 -| 脚本 | 文件 | 说明 | -| ----------------------- | ------------------ | ---------------------------------- | -| `bun run dev` | `scripts/dev.ts` | 同时启动前后端开发服务 | -| `bun run build` | `scripts/build.ts` | Vite 构建 + Bun 编译可执行文件 | -| `bun run test:smoke` | `scripts/smoke.ts` | 构建后的端到端验证 | -| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 | +| 脚本 | 文件 | 说明 | +| -------------------- | ------------------ | ------------------------------ | +| `bun run dev` | `scripts/dev.ts` | 同时启动前后端开发服务 | +| `bun run build` | `scripts/build.ts` | Vite 构建 + Bun 编译可执行文件 | +| `bun run test:smoke` | `scripts/smoke.ts` | 构建后的端到端验证 | +| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 | ### 3.7 环境变量 -| 变量 | 用途 | 默认值 | -| ----------------------- | ------------------------------------------- | ------ | -| `PORT`/`BACKEND_PORT` | 后端监听端口(开发期 Vite 代理目标、生产期监听端口) | `3000` | -| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(仅在 `bun run build` 时有效) | 当前平台 | +| 变量 | 用途 | 默认值 | +| --------------------------- | ---------------------------------------------------- | -------- | +| `PORT`/`BACKEND_PORT` | 后端监听端口(开发期 Vite 代理目标、生产期监听端口) | `3000` | +| `BUN_TARGET`/`BUILD_TARGET` | 交叉编译目标平台(仅在 `bun run build` 时有效) | 当前平台 | ### 3.8 项目配置文件 -| 文件 | 用途 | -| --------------------- | -------------------------------------- | -| `package.json` | 项目信息、脚本、依赖声明 | -| `tsconfig.json` | TypeScript 配置(ESNext 模块、严格模式)| -| `vite.config.ts` | Vite 开发代理与构建配置 | +| 文件 | 用途 | +| --------------------- | ---------------------------------------------- | +| `package.json` | 项目信息、脚本、依赖声明 | +| `tsconfig.json` | TypeScript 配置(ESNext 模块、严格模式) | +| `vite.config.ts` | Vite 开发代理与构建配置 | | `eslint.config.js` | ESLint 规则(含前端不得 import server 的检查) | -| `.prettierrc.json` | Prettier 格式化规则(`printWidth: 120`) | -| `.prettierignore` | Prettier 排除路径 | -| `probes.example.yaml` | 配置文件示例 | -| `opencode.json` | OpenCode 工具配置(TDesign MCP server) | +| `.prettierrc.json` | Prettier 格式化规则(`printWidth: 120`) | +| `.prettierignore` | Prettier 排除路径 | +| `probes.example.yaml` | 配置文件示例 | +| `opencode.json` | OpenCode 工具配置(TDesign MCP server) | ### 3.9 依赖管理 @@ -623,33 +624,77 @@ bun run test:smoke ### 3.10 目录约定 -| 目录 | 约定 | -| --------------- | ------------------------------------------ | -| `src/server/` | 后端代码,不能 import `src/web/` | -| `src/web/` | 前端代码,不能 import `src/server/` | -| `src/shared/` | 前后端共享类型,双向可引用 | -| `scripts/` | 独立运行脚本,可 import 项目源码 | -| `tests/` | 测试目录,结构镜像 src 目录 | -| `dist/` | 构建产物(gitignore) | -| `.build/` | 构建临时文件(gitignore) | -| `openspec/` | OpenSpec 变更管理与规格文档 | -| `data/` | 默认数据目录(gitignore,运行期生成 SQLite)| +| 目录 | 约定 | +| ------------- | -------------------------------------------- | +| `src/server/` | 后端代码,不能 import `src/web/` | +| `src/web/` | 前端代码,不能 import `src/server/` | +| `src/shared/` | 前后端共享类型,双向可引用 | +| `scripts/` | 独立运行脚本,可 import 项目源码 | +| `tests/` | 测试目录,结构镜像 src 目录 | +| `dist/` | 构建产物(gitignore) | +| `.build/` | 构建临时文件(gitignore) | +| `openspec/` | OpenSpec 变更管理与规格文档 | +| `data/` | 默认数据目录(gitignore,运行期生成 SQLite) | --- ## 代码质量 +项目使用多层代码质量保障体系:ESLint 类型感知规则 + Perfectionist 导入排序 + Prettier 格式化 + TypeScript 严格模式 + Git hooks 自动化。 + ```bash -bun run lint # ESLint 检查 +bun run lint # ESLint 检查(含类型感知规则、导入排序、导入验证) bun run format:check # Prettier 格式检查 bun run format # Prettier 自动格式化 -bun run typecheck # TypeScript 类型检查 +bun run typecheck # TypeScript 类型检查(含 noUnusedLocals、noPropertyAccessFromIndexSignature) bun test # 运行所有测试 bun run check # 一键运行 typecheck + lint + format:check + test ``` `check` 是日常开发推荐的质量检查命令。 +### ESLint 规则 + +配置文件:`eslint.config.js` + +| 配置来源 | 用途 | +| ------------------------------------------------- | -------------------------------------------------- | +| `@eslint/js` recommended | JavaScript 基础规则 | +| `typescript-eslint` recommended-type-checked | TypeScript 类型感知规则(no-floating-promises 等) | +| `typescript-eslint` stylistic-type-checked | TypeScript 风格规则(命名规范、语法选择等) | +| `eslint-plugin-perfectionist` recommended-natural | 导入语句和命名导出自动排序 | +| `eslint-plugin-import` | 导入路径验证、循环依赖检测、重复导入合并 | + +### Prettier 配置 + +配置文件:`.prettierrc.json` + +显式声明所有格式化参数(`printWidth: 120`、`semi: true`、`singleQuote: false`、`trailingComma: "all"`、`endOfLine: "lf"` 等),确保不同开发环境产出完全一致的格式化结果。 + +### TypeScript 严格标志 + +| 标志 | 值 | 说明 | +| ------------------------------------ | ----- | -------------------------------------------------------------------------- | +| `strict` | true | 全局严格模式 | +| `noUnusedLocals` | true | 未使用局部变量视为错误 | +| `noUnusedParameters` | false | 保留关闭(路由 handler 统一签名需要,如 `handleXxx(store, method, mode)`) | +| `noPropertyAccessFromIndexSignature` | true | 禁止通过点号访问索引签名属性,强制使用括号语法 | +| `noUncheckedIndexedAccess` | true | 数组/Map 访问必须运行时真值检查 | +| `verbatimModuleSyntax` | true | 强制 `import type` 纯类型导入 | + +### Git Hooks + +通过 husky 在 commit 阶段自动执行检查: + +| Hook | 行为 | +| ------------ | -------------------------------------------------------------- | +| `pre-commit` | lint-staged 对变更文件运行 `eslint --fix` + `prettier --write` | +| `commit-msg` | commitlint 校验提交信息格式 `类型: 简短描述` | + +提交类型限定:`feat`、`fix`、`refactor`、`docs`、`style`、`test`、`chore`。 + +`bun install` 时自动初始化 husky hooks,无需手动配置。 + ## 测试 ```bash diff --git a/bun.lock b/bun.lock index cda026d..4a898cf 100644 --- a/bun.lock +++ b/bun.lock @@ -17,6 +17,8 @@ "xpath": "^0.0.34", }, "devDependencies": { + "@commitlint/cli": "^21.0.0", + "@commitlint/config-conventional": "^21.0.0", "@eslint/js": "^10.0.1", "@tanstack/react-query-devtools": "^5.100.10", "@types/bun": "^1.3.13", @@ -24,8 +26,13 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.3.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", + "husky": "^9.1.7", + "lint-staged": "^17.0.4", "prettier": "^3.8.3", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2", @@ -68,6 +75,42 @@ "@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@commitlint/cli": ["@commitlint/cli@21.0.0", "https://registry.npmmirror.com/@commitlint/cli/-/cli-21.0.0.tgz", { "dependencies": { "@commitlint/format": "^21.0.0", "@commitlint/lint": "^21.0.0", "@commitlint/load": "^21.0.0", "@commitlint/read": "^21.0.0", "@commitlint/types": "^21.0.0", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-p3y2oC0G2R45zaadMwBxCiSesS8digi5RDplP3Zrfpzm7xIgrgAj0W4fGzONjpHyg8obDVJDU45g5txzeMcblg=="], + + "@commitlint/config-conventional": ["@commitlint/config-conventional@21.0.0", "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "conventional-changelog-conventionalcommits": "^9.2.0" } }, "sha512-QJX/rPK4Yu3f5J4OCIBy5aXq2e0EEdwSDFZ3NQvFAXTm3gs12ipyZ+yjhZxm3hHn6DB8wuv3zhFTL1I2tYzUBA=="], + + "@commitlint/config-validator": ["@commitlint/config-validator@21.0.0", "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "ajv": "^8.11.0" } }, "sha512-v0UplTYryNUB463X5WrelzKq5/qyYm9/iUNk38S7ZLnd56Uuk2T9awhYKGlgD2/4L5YuN2gsKkyy4EHpRPPz2Q=="], + + "@commitlint/ensure": ["@commitlint/ensure@21.0.0", "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "es-toolkit": "^1.46.0" } }, "sha512-n+OYs0Ws9GKC2WlmAeLNoPz9CUg6n/ZyYMkFF8rJ0aMn2kDTDTG0VqK/2Dco0EB4fhuF3JPIllJmU9/LKTl4aw=="], + + "@commitlint/execute-rule": ["@commitlint/execute-rule@21.0.0", "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-21.0.0.tgz", {}, "sha512-3OhTq2gQX1tEheMsbDNqxfcNHsAM6g9cub9plf05I9jCxtbNfn8Y+mhClKyUwhX4dbtmC4OLZ9i+HNmoL1aksA=="], + + "@commitlint/format": ["@commitlint/format@21.0.0", "https://registry.npmmirror.com/@commitlint/format/-/format-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "picocolors": "^1.1.1" } }, "sha512-RTfGSrueEgofs1piqwi42U05d85wfxiMH2ncMCZnltx1XqPR3N2S48oACBtTy4xRAhWlf5XlHkK2RaDzEQu3dA=="], + + "@commitlint/is-ignored": ["@commitlint/is-ignored@21.0.0", "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "semver": "^7.6.0" } }, "sha512-K3SaaOTVY9VKhge7vl0R3ng7GENRzJQ9MPV43Tu53kAwEgSx/E0HF4US3AcVqdvlvsDUbF2yXvED95dhela83w=="], + + "@commitlint/lint": ["@commitlint/lint@21.0.0", "https://registry.npmmirror.com/@commitlint/lint/-/lint-21.0.0.tgz", { "dependencies": { "@commitlint/is-ignored": "^21.0.0", "@commitlint/parse": "^21.0.0", "@commitlint/rules": "^21.0.0", "@commitlint/types": "^21.0.0" } }, "sha512-dlUJA0Ka14R1YaR46JVRWE3m/8dOQAgE/D0heUfzYua5Jogtq/zzu2ITAIaB/u25DaKjtEO6kuvASzsFDyrPMw=="], + + "@commitlint/load": ["@commitlint/load@21.0.0", "https://registry.npmmirror.com/@commitlint/load/-/load-21.0.0.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.0", "@commitlint/execute-rule": "^21.0.0", "@commitlint/resolve-extends": "^21.0.0", "@commitlint/types": "^21.0.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", "is-plain-obj": "^4.1.0", "picocolors": "^1.1.1" } }, "sha512-l0nBfO/20PKcJXHZqDIgh7kw/TWVVwn8zZJOkVGBK/ig/h328jBu9jK7OiDl2oZr5mLphmKGjYDR2ffEyb2lIA=="], + + "@commitlint/message": ["@commitlint/message@21.0.0", "https://registry.npmmirror.com/@commitlint/message/-/message-21.0.0.tgz", {}, "sha512-+daU92JaOHhI2En9KcH+2mvZGJ6D4YSxb/32QDwqkOwSj1Vanjio8PbAqX7dneACdg6B7RgQ7i3mpyYZAws4nw=="], + + "@commitlint/parse": ["@commitlint/parse@21.0.0", "https://registry.npmmirror.com/@commitlint/parse/-/parse-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "conventional-changelog-angular": "^8.2.0", "conventional-commits-parser": "^6.3.0" } }, "sha512-1dbvFBcQK79aTbpc2QCrgEDc6/MMkQ0Mdz4gGmYkN4AHMnAK9HesSewTHqGTrW5mALrMlYSgcWyvKjloY2w19A=="], + + "@commitlint/read": ["@commitlint/read@21.0.0", "https://registry.npmmirror.com/@commitlint/read/-/read-21.0.0.tgz", { "dependencies": { "@commitlint/top-level": "^21.0.0", "@commitlint/types": "^21.0.0", "git-raw-commits": "^5.0.0", "tinyexec": "^1.0.0" } }, "sha512-8VKLKLl2vBSKoTMm1LwcySsyxrBeotnqcT5qJi9pPuPfqSapdAD870Ckgh79c41UFywL6kMqtiyY+kxtfcqZGg=="], + + "@commitlint/resolve-extends": ["@commitlint/resolve-extends@21.0.0", "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-21.0.0.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.0", "@commitlint/types": "^21.0.0", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" } }, "sha512-hrJYSZRpmecmSoxYrpuJ/1Q4J9JHt4AVVtr5/Ac6upLO/jJ1DnIm2AjD+38gru3KGOec4aHCVqETuWWLJhydWw=="], + + "@commitlint/rules": ["@commitlint/rules@21.0.0", "https://registry.npmmirror.com/@commitlint/rules/-/rules-21.0.0.tgz", { "dependencies": { "@commitlint/ensure": "^21.0.0", "@commitlint/message": "^21.0.0", "@commitlint/to-lines": "^21.0.0", "@commitlint/types": "^21.0.0" } }, "sha512-NgQhX1qENA+rbrMw5KKyvVZpZG4D/0wgK8Z4INtcwKbfKtVDFMbn0oNc/Rs8wdyBPBj7ue8Lo/GllUL2Mqjwkg=="], + + "@commitlint/to-lines": ["@commitlint/to-lines@21.0.0", "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-21.0.0.tgz", {}, "sha512-qMwvrJK/x3dPcXsIAtQAMKV5Q0wTioyqyHKR06vVN4wmBF4cCrrLq5x81FDeY3Ba+GWgDt0/P3Zw/IHGM8lwgg=="], + + "@commitlint/top-level": ["@commitlint/top-level@21.0.0", "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-21.0.0.tgz", { "dependencies": { "escalade": "^3.2.0" } }, "sha512-8jPqyWZueuN4hU6/ArKVsZ6i8xWtjIrbzHEOaLaTGUfjhhbZNBfXef/DGjzxy55hAv3yFNxHLINfI1bCJ0/MzA=="], + + "@commitlint/types": ["@commitlint/types@21.0.0", "https://registry.npmmirror.com/@commitlint/types/-/types-21.0.0.tgz", { "dependencies": { "conventional-commits-parser": "^6.3.0", "picocolors": "^1.1.1" } }, "sha512-6nEz+M7I90iix4sviA8NLwskOuyt0M98KUU2aYgiKbn46jMSxUm1l2ACtzRd9ec+y38aKyJhW4Fp6NW0z35kJQ=="], + + "@conventional-changelog/git-client": ["@conventional-changelog/git-client@2.7.0", "https://registry.npmmirror.com/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", { "dependencies": { "@simple-libs/child-process-utils": "^1.0.0", "@simple-libs/stream-utils": "^1.2.0", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.4.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw=="], + "@emnapi/core": ["@emnapi/core@1.10.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], @@ -110,7 +153,7 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="], "@oxc-project/types": ["@oxc-project/types@0.128.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.128.0.tgz", {}, "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ=="], @@ -150,6 +193,12 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@simple-libs/child-process-utils": ["@simple-libs/child-process-utils@1.0.2", "https://registry.npmmirror.com/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", { "dependencies": { "@simple-libs/stream-utils": "^1.2.0" } }, "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw=="], + + "@simple-libs/stream-utils": ["@simple-libs/stream-utils@1.2.0", "https://registry.npmmirror.com/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", {}, "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "https://registry.npmmirror.com/@standard-schema/utils/-/utils-0.3.0.tgz", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], @@ -190,6 +239,8 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/json5": ["@types/json5@0.0.29", "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/node": ["@types/node@25.6.2", "https://registry.npmmirror.com/@types/node/-/node-25.6.2.tgz", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -222,6 +273,44 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="], + "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], + + "@unrs/resolver-binding-android-arm64": ["@unrs/resolver-binding-android-arm64@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g=="], + + "@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g=="], + + "@unrs/resolver-binding-darwin-x64": ["@unrs/resolver-binding-darwin-x64@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ=="], + + "@unrs/resolver-binding-freebsd-x64": ["@unrs/resolver-binding-freebsd-x64@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw=="], + + "@unrs/resolver-binding-linux-arm-gnueabihf": ["@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw=="], + + "@unrs/resolver-binding-linux-arm-musleabihf": ["@unrs/resolver-binding-linux-arm-musleabihf@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw=="], + + "@unrs/resolver-binding-linux-arm64-gnu": ["@unrs/resolver-binding-linux-arm64-gnu@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ=="], + + "@unrs/resolver-binding-linux-arm64-musl": ["@unrs/resolver-binding-linux-arm64-musl@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w=="], + + "@unrs/resolver-binding-linux-ppc64-gnu": ["@unrs/resolver-binding-linux-ppc64-gnu@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA=="], + + "@unrs/resolver-binding-linux-riscv64-gnu": ["@unrs/resolver-binding-linux-riscv64-gnu@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ=="], + + "@unrs/resolver-binding-linux-riscv64-musl": ["@unrs/resolver-binding-linux-riscv64-musl@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew=="], + + "@unrs/resolver-binding-linux-s390x-gnu": ["@unrs/resolver-binding-linux-s390x-gnu@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg=="], + + "@unrs/resolver-binding-linux-x64-gnu": ["@unrs/resolver-binding-linux-x64-gnu@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w=="], + + "@unrs/resolver-binding-linux-x64-musl": ["@unrs/resolver-binding-linux-x64-musl@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA=="], + + "@unrs/resolver-binding-wasm32-wasi": ["@unrs/resolver-binding-wasm32-wasi@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", { "dependencies": { "@napi-rs/wasm-runtime": "^0.2.11" }, "cpu": "none" }, "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ=="], + + "@unrs/resolver-binding-win32-arm64-msvc": ["@unrs/resolver-binding-win32-arm64-msvc@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw=="], + + "@unrs/resolver-binding-win32-ia32-msvc": ["@unrs/resolver-binding-win32-ia32-msvc@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ=="], + + "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="], @@ -232,6 +321,32 @@ "ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + + "ansi-regex": ["ansi-regex@6.2.2", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-ify": ["array-ify@1.0.0", "https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], + + "array-includes": ["array-includes@3.1.9", "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "async-function": ["async-function@1.0.0", "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.28", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.28.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA=="], @@ -244,6 +359,14 @@ "bun-types": ["bun-types@1.3.13", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.13.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="], + "call-bind": ["call-bind@1.0.9", "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "caniuse-lite": ["caniuse-lite@1.0.30001792", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], "cheerio": ["cheerio@1.2.0", "https://registry.npmmirror.com/cheerio/-/cheerio-1.2.0.tgz", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], @@ -252,10 +375,30 @@ "classnames": ["classnames@2.5.1", "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + "cli-cursor": ["cli-cursor@5.0.0", "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@5.2.0", "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.2.0.tgz", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="], + + "cliui": ["cliui@9.0.1", "https://registry.npmmirror.com/cliui/-/cliui-9.0.1.tgz", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], + "clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "compare-func": ["compare-func@2.0.0", "https://registry.npmmirror.com/compare-func/-/compare-func-2.0.0.tgz", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + + "concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "conventional-changelog-angular": ["conventional-changelog-angular@8.3.1", "https://registry.npmmirror.com/conventional-changelog-angular/-/conventional-changelog-angular-8.3.1.tgz", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-6gfI3otXK5Ph5DfCOI1dblr+kN3FAm5a97hYoQkqNZxOaYa5WKfXH+AnpsmS+iUH2mgVC2Cg2Qw9m5OKcmNrIg=="], + + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@9.3.1", "https://registry.npmmirror.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-9.3.1.tgz", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-dTYtpIacRpcZgrvBYvBfArMmK2xvIpv2TaxM0/ZI5CBtNUzvF2x0t15HsbRABWprS6UPmvj+PzHVjSx4qAVKyw=="], + + "conventional-commits-parser": ["conventional-commits-parser@6.4.0", "https://registry.npmmirror.com/conventional-commits-parser/-/conventional-commits-parser-6.4.0.tgz", { "dependencies": { "@simple-libs/stream-utils": "^1.2.0", "meow": "^13.0.0" }, "bin": { "conventional-commits-parser": "dist/cli/index.js" } }, "sha512-tvRg7FIBNlyPzjdG8wWRlPHQJJHI7DylhtRGeU9Lq+JuoPh5BKpPRX83ZdLrvXuOSu5Eo/e7SzOQhU4Hd2Miuw=="], + "convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cosmiconfig": ["cosmiconfig@9.0.1", "https://registry.npmmirror.com/cosmiconfig/-/cosmiconfig-9.0.1.tgz", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="], + + "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.3.0", "https://registry.npmmirror.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.3.0.tgz", { "dependencies": { "jiti": "2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA=="], + "cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "css-select": ["css-select@5.2.2", "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], @@ -286,6 +429,12 @@ "d3-timer": ["d3-timer@3.0.1", "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + "data-view-buffer": ["data-view-buffer@1.0.2", "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "dayjs": ["dayjs@1.11.10", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz", {}, "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="], "debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -294,8 +443,14 @@ "deep-is": ["deep-is@0.1.4", "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "define-data-property": ["define-data-property@1.1.4", "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-properties": ["define-properties@1.2.1", "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "doctrine": ["doctrine@2.1.0", "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + "dom-helpers": ["dom-helpers@5.2.1", "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], "dom-serializer": ["dom-serializer@2.0.0", "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], @@ -306,12 +461,38 @@ "domutils": ["domutils@3.2.2", "https://registry.npmmirror.com/domutils/-/domutils-3.2.2.tgz", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dot-prop": ["dot-prop@5.3.0", "https://registry.npmmirror.com/dot-prop/-/dot-prop-5.3.0.tgz", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + + "dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "electron-to-chromium": ["electron-to-chromium@1.5.353", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", {}, "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w=="], + "emoji-regex": ["emoji-regex@10.6.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "encoding-sniffer": ["encoding-sniffer@0.2.1", "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], "entities": ["entities@4.5.0", "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "env-paths": ["env-paths@2.2.1", "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "environment": ["environment@1.1.0", "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-ex": ["error-ex@1.3.4", "https://registry.npmmirror.com/error-ex/-/error-ex-1.3.4.tgz", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-abstract": ["es-abstract@1.24.2", "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], + + "es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "es-toolkit": ["es-toolkit@1.46.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.46.1.tgz", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], "escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -320,6 +501,18 @@ "eslint": ["eslint@10.3.0", "https://registry.npmmirror.com/eslint/-/eslint-10.3.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw=="], + "eslint-import-context": ["eslint-import-context@0.1.9", "https://registry.npmmirror.com/eslint-import-context/-/eslint-import-context-0.1.9.tgz", { "dependencies": { "get-tsconfig": "^4.10.1", "stable-hash-x": "^0.2.0" }, "peerDependencies": { "unrs-resolver": "^1.0.0" }, "optionalPeers": ["unrs-resolver"] }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], + + "eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@4.4.4", "https://registry.npmmirror.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", { "dependencies": { "debug": "^4.4.1", "eslint-import-context": "^0.1.8", "get-tsconfig": "^4.10.1", "is-bun-module": "^2.0.0", "stable-hash-x": "^0.2.0", "tinyglobby": "^0.2.14", "unrs-resolver": "^1.7.11" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import", "eslint-plugin-import-x"] }, "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-plugin-perfectionist": ["eslint-plugin-perfectionist@5.9.0", "https://registry.npmmirror.com/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-5.9.0.tgz", { "dependencies": { "@typescript-eslint/utils": "^8.58.2", "natural-orderby": "^5.0.0" }, "peerDependencies": { "eslint": "^8.45.0 || ^9.0.0 || ^10.0.0" } }, "sha512-8TWzg02zmnBdZwCkWLi8jhzqXI+fE7Z/RwV8SL6xD45tJ8Bp3wGuYL2XtQgfe/Wd0eBqOUX+s6ey73IyszvKTA=="], + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="], @@ -346,6 +539,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-uri": ["fast-uri@3.1.2", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.2.tgz", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + "fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], @@ -356,12 +551,54 @@ "flatted": ["flatted@3.4.2", "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "for-each": ["for-each@0.3.5", "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + "fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + "gensync": ["gensync@1.0.0-beta.2", "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "get-tsconfig": ["get-tsconfig@4.14.0", "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + + "git-raw-commits": ["git-raw-commits@5.0.1", "https://registry.npmmirror.com/git-raw-commits/-/git-raw-commits-5.0.1.tgz", { "dependencies": { "@conventional-changelog/git-client": "^2.6.0", "meow": "^13.0.0" }, "bin": { "git-raw-commits": "src/cli.js" } }, "sha512-Y+csSm2GD/PCSh6Isd/WiMjNAydu0VBiG9J7EdQsNA5P9uXvLayqjmTsNlK5Gs9IhblFZqOU0yid5Il5JPoLiQ=="], + "glob-parent": ["glob-parent@6.0.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "global-directory": ["global-directory@5.0.0", "https://registry.npmmirror.com/global-directory/-/global-directory-5.0.0.tgz", { "dependencies": { "ini": "6.0.0" } }, "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w=="], + + "globalthis": ["globalthis@1.0.4", "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-bigints": ["has-bigints@1.1.0", "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.3", "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "hermes-estree": ["hermes-estree@0.25.1", "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "hermes-parser": ["hermes-parser@0.25.1", "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], @@ -370,33 +607,103 @@ "htmlparser2": ["htmlparser2@10.1.0", "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.1.0.tgz", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + "husky": ["husky@9.1.7", "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "iconv-lite": ["iconv-lite@0.6.3", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "ignore": ["ignore@5.3.2", "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "immer": ["immer@10.2.0", "https://registry.npmmirror.com/immer/-/immer-10.2.0.tgz", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="], + "import-fresh": ["import-fresh@3.3.1", "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "imurmurhash": ["imurmurhash@0.1.4", "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "ini": ["ini@6.0.0", "https://registry.npmmirror.com/ini/-/ini-6.0.0.tgz", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + + "internal-slot": ["internal-slot@1.1.0", "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + "internmap": ["internmap@2.0.3", "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + "is-array-buffer": ["is-array-buffer@3.0.5", "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.2.1", "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.2.1.tgz", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-async-function": ["is-async-function@2.1.1", "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-bun-module": ["is-bun-module@2.0.0", "https://registry.npmmirror.com/is-bun-module/-/is-bun-module-2.0.0.tgz", { "dependencies": { "semver": "^7.7.1" } }, "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ=="], + + "is-callable": ["is-callable@1.2.7", "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.2", "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.2.tgz", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "is-data-view": ["is-data-view@1.0.2", "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + "is-extglob": ["is-extglob@2.1.1", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "is-generator-function": ["is-generator-function@1.1.2", "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + "is-glob": ["is-glob@4.0.3", "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-map": ["is-map@2.0.3", "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number-object": ["is-number-object@1.1.1", "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-obj": ["is-obj@2.0.0", "https://registry.npmmirror.com/is-obj/-/is-obj-2.0.0.tgz", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-regex": ["is-regex@1.2.1", "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jiti": ["jiti@2.6.1", "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + "js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.1.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsesc": ["jsesc@3.1.0", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json-buffer": ["json-buffer@3.0.1", "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "https://registry.npmmirror.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "json5": ["json5@1.0.2", "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], "keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -426,36 +733,80 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lint-staged": ["lint-staged@17.0.4", "https://registry.npmmirror.com/lint-staged/-/lint-staged-17.0.4.tgz", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.1.2" }, "optionalDependencies": { "yaml": "^2.8.4" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA=="], + + "listr2": ["listr2@10.2.1", "https://registry.npmmirror.com/listr2/-/listr2-10.2.1.tgz", { "dependencies": { "cli-truncate": "^5.2.0", "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^10.0.0" } }, "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q=="], + "locate-path": ["locate-path@6.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "lodash-es": ["lodash-es@4.18.1", "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + "log-update": ["log-update@6.1.0", "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + "loose-envify": ["loose-envify@1.4.0", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "meow": ["meow@13.2.0", "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="], + + "mimic-function": ["mimic-function@5.0.1", "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "minimist": ["minimist@1.2.8", "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "mitt": ["mitt@3.0.1", "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.12", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], + "napi-postinstall": ["napi-postinstall@0.3.4", "https://registry.npmmirror.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], + "natural-compare": ["natural-compare@1.4.0", "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "natural-orderby": ["natural-orderby@5.0.0", "https://registry.npmmirror.com/natural-orderby/-/natural-orderby-5.0.0.tgz", {}, "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg=="], + + "node-exports-info": ["node-exports-info@1.6.0", "https://registry.npmmirror.com/node-exports-info/-/node-exports-info-1.6.0.tgz", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + "node-releases": ["node-releases@2.0.38", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.38.tgz", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], "nth-check": ["nth-check@2.1.1", "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.3.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "onetime": ["onetime@7.0.0", "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + "optionator": ["optionator@0.9.4", "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "own-keys": ["own-keys@1.0.1", "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "p-limit": ["p-limit@3.1.0", "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "parent-module": ["parent-module@1.0.1", "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -466,12 +817,16 @@ "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "performance-now": ["performance-now@2.1.0", "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], "picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + "postcss": ["postcss@8.5.14", "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="], "prelude-ls": ["prelude-ls@1.2.1", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -502,48 +857,124 @@ "redux-thunk": ["redux-thunk@3.1.0", "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + "regenerator-runtime": ["regenerator-runtime@0.14.1", "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "reselect": ["reselect@5.1.1", "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "resolve": ["resolve@2.0.0-next.6", "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.6.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + + "resolve-from": ["resolve-from@5.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "restore-cursor": ["restore-cursor@5.1.0", "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + "rolldown": ["rolldown@1.0.0-rc.18", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.18.tgz", { "dependencies": { "@oxc-project/types": "=0.128.0", "@rolldown/pluginutils": "1.0.0-rc.18" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", "@rolldown/binding-darwin-x64": "1.0.0-rc.18", "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg=="], + "safe-array-concat": ["safe-array-concat@1.1.4", "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "set-function-length": ["set-function-length@1.2.2", "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slice-ansi": ["slice-ansi@8.0.0", "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-8.0.0.tgz", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="], + "sortablejs": ["sortablejs@1.15.7", "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.7.tgz", {}, "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A=="], "source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "stable-hash-x": ["stable-hash-x@0.2.0", "https://registry.npmmirror.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "string-argv": ["string-argv@0.3.2", "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@7.2.0", "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "strip-ansi": ["strip-ansi@7.2.0", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "strip-bom": ["strip-bom@3.0.0", "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + "tdesign-icons-react": ["tdesign-icons-react@0.6.4", "https://registry.npmmirror.com/tdesign-icons-react/-/tdesign-icons-react-0.6.4.tgz", { "dependencies": { "@babel/runtime": "^7.16.5", "classnames": "^2.2.6" }, "peerDependencies": { "react": ">=16.13.1", "react-dom": ">=16.13.1" } }, "sha512-USAoi9vBWcwcJT45VqR3dRqX1MeAsn/RhHVx4bLwplhrlvE80ZQ1N9V+6F3HqE1Qe9mMDbtRM8Ul80+lALScww=="], "tdesign-react": ["tdesign-react@1.16.9", "https://registry.npmmirror.com/tdesign-react/-/tdesign-react-1.16.9.tgz", { "dependencies": { "@babel/runtime": "~7.26.7", "@popperjs/core": "~2.11.2", "@types/sortablejs": "^1.10.7", "@types/validator": "^13.1.3", "classnames": "~2.5.1", "dayjs": "1.11.10", "hoist-non-react-statics": "~3.3.2", "lodash-es": "^4.17.21", "mitt": "^3.0.0", "raf": "~3.4.1", "react-fast-compare": "^3.2.2", "react-is": "^18.2.0", "react-transition-group": "~4.4.1", "sortablejs": "^1.15.0", "tdesign-icons-react": "^0.6.4", "tslib": "~2.3.1", "validator": "~13.15.0" }, "peerDependencies": { "react": ">=16.13.1", "react-dom": ">=16.13.1" } }, "sha512-C3uZRTkJ1iQ62BrMkuvqvBK+4HEuhl82rABxa6kAHGHL3eBI4DPfzAJGF0T3b+DKCBeJxb0x10elumT6NkQEaw=="], "tiny-invariant": ["tiny-invariant@1.3.3", "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + "tinyexec": ["tinyexec@1.1.2", "https://registry.npmmirror.com/tinyexec/-/tinyexec-1.1.2.tgz", {}, "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA=="], + "tinyglobby": ["tinyglobby@0.2.16", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "ts-api-utils": ["ts-api-utils@2.5.0", "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="], + "tsconfig-paths": ["tsconfig-paths@3.15.0", "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + "tslib": ["tslib@2.3.1", "https://registry.npmmirror.com/tslib/-/tslib-2.3.1.tgz", {}, "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="], "type-check": ["type-check@0.4.0", "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "typed-array-buffer": ["typed-array-buffer@1.0.3", "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.7.tgz", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typescript": ["typescript@6.0.3", "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "typescript-eslint": ["typescript-eslint@8.59.2", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="], + "unbox-primitive": ["unbox-primitive@1.1.0", "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + "undici": ["undici@7.25.0", "https://registry.npmmirror.com/undici/-/undici-7.25.0.tgz", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="], "undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], + "unrs-resolver": ["unrs-resolver@1.11.1", "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], @@ -562,18 +993,44 @@ "which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.20.tgz", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + "word-wrap": ["word-wrap@1.2.5", "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrap-ansi": ["wrap-ansi@10.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-10.0.0.tgz", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="], + "xpath": ["xpath@0.0.34", "https://registry.npmmirror.com/xpath/-/xpath-0.0.34.tgz", {}, "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA=="], + "y18n": ["y18n@5.0.8", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.9.0", "https://registry.npmmirror.com/yaml/-/yaml-2.9.0.tgz", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "yargs": ["yargs@18.0.0", "https://registry.npmmirror.com/yargs/-/yargs-18.0.0.tgz", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], + + "yargs-parser": ["yargs-parser@22.0.0", "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-22.0.0.tgz", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], + "yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zod": ["zod@4.4.3", "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "zod-validation-error": ["zod-validation-error@4.0.2", "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "@babel/core/json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "@commitlint/config-validator/ajv": ["ajv@8.20.0", "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "@commitlint/is-ignored/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + + "@conventional-changelog/git-client/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "@emnapi/core/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@emnapi/runtime/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -584,16 +1041,38 @@ "@reduxjs/toolkit/immer": ["immer@11.1.8", "https://registry.npmmirror.com/immer/-/immer-11.1.8.tgz", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="], + "@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="], + "@tybys/wasm-util/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + "cli-truncate/string-width": ["string-width@8.2.1", "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + + "cliui/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "htmlparser2/entities": ["entities@7.0.1", "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "import-fresh/resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "is-bun-module/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.2", "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "log-update/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "prop-types/react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], @@ -603,5 +1082,13 @@ "tdesign-react/@babel/runtime": ["@babel/runtime@7.26.10", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.10.tgz", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="], "tdesign-react/react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "wrap-ansi/string-width": ["string-width@8.2.1", "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="], + + "@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.14", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="], + + "eslint-plugin-import/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..5ca3350 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,8 @@ +export default { + extends: ["@commitlint/config-conventional"], + rules: { + "subject-case": [0], + "subject-full-stop": [0], + "type-enum": [2, "always", ["feat", "fix", "refactor", "docs", "style", "test", "chore"]], + }, +}; diff --git a/eslint.config.js b/eslint.config.js index 4880793..843e901 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,6 @@ import js from "@eslint/js"; +import importPlugin from "eslint-plugin-import"; +import perfectionist from "eslint-plugin-perfectionist"; import reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import tseslint from "typescript-eslint"; @@ -14,16 +16,47 @@ export default tseslint.config( ".opencode/**", ".claude/**", ".codex/**", + ".agents/**", + "bun.lock", + "data/**", ], }, js.configs.recommended, - ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + importPlugin.flatConfigs.recommended, + importPlugin.flatConfigs.typescript, + perfectionist.configs["recommended-natural"], + { + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + settings: { + "import/resolver": { node: true, typescript: true }, + }, + }, { - files: ["**/*.{ts,tsx}"], rules: { + "@typescript-eslint/array-type": ["error", { default: "array-simple" }], + "@typescript-eslint/consistent-type-assertions": ["error", { assertionStyle: "as" }], + "@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }], + "@typescript-eslint/only-throw-error": "error", + "@typescript-eslint/prefer-nullish-coalescing": "error", + "@typescript-eslint/prefer-optional-chain": "error", + "import/no-unresolved": ["error", { ignore: ["^bun:"] }], "no-undef": "off", }, }, + { + files: ["eslint.config.js"], + rules: { + "import/no-named-as-default": "off", + "import/no-named-as-default-member": "off", + }, + }, { files: ["src/web/**/*.{ts,tsx}"], plugins: { @@ -32,7 +65,6 @@ export default tseslint.config( }, rules: { ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], "no-restricted-imports": [ "error", { @@ -53,6 +85,7 @@ export default tseslint.config( ], }, ], + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], }, }, ); diff --git a/openspec/specs/code-quality-gates/spec.md b/openspec/specs/code-quality-gates/spec.md index 7d511b4..f3160e6 100644 --- a/openspec/specs/code-quality-gates/spec.md +++ b/openspec/specs/code-quality-gates/spec.md @@ -5,7 +5,7 @@ ## Requirements ### Requirement: ESLint 代码质量门禁 -项目 SHALL 提供 ESLint 代码质量门禁,用于审查 TypeScript、React 前端、脚本和测试代码中的质量问题。 +项目 SHALL 提供 ESLint 代码质量门禁,用于审查 TypeScript、React 前端、脚本和测试代码中的质量问题。ESLint 配置 SHALL 包括 `@eslint/js` recommended 规则、`typescript-eslint` recommended-type-checked 和 stylistic-type-checked 规则、`eslint-plugin-perfectionist` 导入排序规则、`eslint-plugin-import` 导入验证规则,以及精选的单项类型安全和风格规则。 #### Scenario: 运行 lint 检查 - **WHEN** 开发者运行文档化的 lint 命令 @@ -19,8 +19,24 @@ - **WHEN** `src/web` 前端代码导入 `src/server` 后端运行时实现 - **THEN** lint 命令 MUST 失败并报告前后端边界违规 +#### Scenario: 检测类型安全违规 +- **WHEN** 代码中存在浮动的 Promise 未 await、any 类型泄漏到明确类型位置、模板字符串中包含非字符串化对象等类型安全隐患 +- **THEN** lint 命令 MUST 失败并报告对应 `@typescript-eslint` 规则违规 + +#### Scenario: 检测导入路径错误 +- **WHEN** 代码中导入路径指向不存在的文件或已废弃的路径 +- **THEN** lint 命令 MUST 失败并报告 `import/no-unresolved` 或 `import/no-deprecated` 错误 + +#### Scenario: 排除第三方模板目录 +- **WHEN** ESLint 运行检查 +- **THEN** 系统 MUST 排除 `.agents/` 等第三方模板目录,不检查其中的代码 + +#### Scenario: 排除生成产物和锁文件 +- **WHEN** ESLint 运行检查 +- **THEN** 系统 MUST 排除 `dist/`、`.build/`、`node_modules/`、`openspec/`、`.opencode/`、`.claude/`、`.codex/`、`*.bun-build`、`bun.lock`、`data/` 等非源码目录 + ### Requirement: Prettier 代码格式门禁 -项目 SHALL 提供 Prettier 格式化和格式检查命令,用于统一代码风格。 +项目 SHALL 提供 Prettier 格式化和格式检查命令,用于统一代码风格。Prettier 配置 SHALL 显式声明 `printWidth`、`semi`、`singleQuote`、`trailingComma`、`bracketSpacing`、`arrowParens`、`endOfLine`、`tabWidth`、`useTabs` 全部格式化参数。 #### Scenario: 检查代码格式 - **WHEN** 开发者运行文档化的格式检查命令 @@ -32,7 +48,51 @@ #### Scenario: 排除 OpenSpec 文档和生成产物 - **WHEN** Prettier 格式化或格式检查运行 -- **THEN** 系统 MUST 排除 `openspec/`、`dist/`、`.build/`、`node_modules/`、`bun.lock` 和临时构建产物 +- **THEN** 系统 MUST 排除 `openspec/`、`dist/`、`.build/`、`node_modules/`、`bun.lock`、`skills-lock.json`、`.agents/`、`data/`、`*.bun-build`、`.opencode/`、`.claude/`、`.codex/` 和临时构建产物 + +#### Scenario: 格式化配置一致性 +- **WHEN** 不同开发者在不同操作系统上运行 `prettier --write` +- **THEN** 由于所有格式化参数均显式定义,产物 SHALL 完全一致 + +### Requirement: TypeScript 未使用变量检测 +项目 SHALL 启用 TypeScript `noUnusedLocals` 编译选项,将未使用的局部变量检测为编译错误。 + +#### Scenario: 存在未使用的局部变量 +- **WHEN** TypeScript 代码中存在声明但未被引用的局部变量 +- **THEN** `tsc --noEmit` MUST 以非零状态退出并报告未使用变量 + +### Requirement: TypeScript 索引签名属性访问检测 +项目 SHALL 启用 TypeScript `noPropertyAccessFromIndexSignature` 编译选项,禁止通过点号访问未显式声明的属性。 + +#### Scenario: 通过点号访问 Record 动态属性 +- **WHEN** 代码通过 `.property` 点号语法访问 `Record` 类型或索引签名类型的属性 +- **THEN** `tsc --noEmit` MUST 以非零状态退出,强制使用 `["property"]` 括号语法显式访问 + +### Requirement: ESLint 导入自动排序 +项目 SHALL 通过 `eslint-plugin-perfectionist` 对导入语句进行自动排序,确保导入顺序一致性。 + +#### Scenario: 导入语句无序排列 +- **WHEN** 文件中导入语句未按要求排序 +- **THEN** `eslint --fix` SHALL 自动重排 import 声明和 named imports 内部顺序 + +#### Scenario: type import 与 value import 混合 +- **WHEN** 文件中同时存在 `import type` 和 `import` 语句 +- **THEN** perfectionist SHALL 正确识别并分别排序,不将 type 和 value 导入混淆 + +### Requirement: ESLint 导入路径验证 +项目 SHALL 通过 `eslint-plugin-import` 验证导入路径的有效性和一致性。 + +#### Scenario: 导入不存在的模块路径 +- **WHEN** 代码中导入了不存在或路径错误的模块 +- **THEN** lint 命令 MUST 失败并报告 `import/no-unresolved` 错误 + +#### Scenario: 存在重复导入 +- **WHEN** 同一个模块在同一文件中被多次导入 +- **THEN** `eslint --fix` SHALL 自动合并重复导入为目标模块的单条导入 + +#### Scenario: 存在循环依赖 +- **WHEN** 模块 A 导入模块 B,同时模块 B 导入模块 A +- **THEN** lint 命令 MUST 报告 `import/no-cycle` 警告 ### Requirement: 快速检查命令 项目 SHALL 提供快速 `check` 命令,用于日常开发期间验证代码质量和基础行为。 diff --git a/openspec/specs/commit-quality-gates/spec.md b/openspec/specs/commit-quality-gates/spec.md new file mode 100644 index 0000000..18b4885 --- /dev/null +++ b/openspec/specs/commit-quality-gates/spec.md @@ -0,0 +1,50 @@ +## Purpose + +定义 Git hooks 自动化质量门禁行为,在 pre-commit 阶段自动运行代码检查和格式化,在 commit-msg 阶段校验提交信息格式。 + +## Requirements + +### Requirement: pre-commit 自动质量检查 +项目 SHALL 通过 husky 和 lint-staged 在 git commit 前自动对变更文件运行 ESLint 和 Prettier 检查。 + +#### Scenario: 变更 TypeScript 文件后提交 +- **WHEN** 开发者 stage 了 `.ts` 或 `.tsx` 文件并执行 `git commit` +- **THEN** lint-staged SHALL 自动对变更文件运行 `eslint --fix` 和 `prettier --write`,修复后继续提交 + +#### Scenario: 变更 Markdown 或 JSON 文件后提交 +- **WHEN** 开发者 stage 了 `.md`、`.json`、`.yaml` 或 `.yml` 文件并执行 `git commit` +- **THEN** lint-staged SHALL 自动对变更文件运行 `prettier --write` + +#### Scenario: lint 检查失败阻止提交 +- **WHEN** 变更文件存在无法自动修复的 ESLint 错误 +- **THEN** pre-commit hook MUST 以非零状态退出,阻止提交 + +#### Scenario: 无变更文件提交 +- **WHEN** 开发者执行 `git commit` 但无 stage 文件 +- **THEN** lint-staged SHALL 正常通过,不阻止提交 + +### Requirement: 提交信息格式校验 +项目 SHALL 通过 commitlint 在 git commit 时校验提交信息必须符合 "类型: 简短描述" 格式,类型限定为 feat/fix/refactor/docs/style/test/chore。 + +#### Scenario: 有效的中文提交信息 +- **WHEN** 开发者提交信息为 "feat: 新增导入排序功能" +- **THEN** commit-msg hook SHALL 通过校验 + +#### Scenario: 缺少类型前缀的提交信息 +- **WHEN** 开发者提交信息为 "新增导入排序功能"(无 "feat:" 前缀) +- **THEN** commit-msg hook MUST 以非零状态退出,提示正确格式 + +#### Scenario: 无效的提交类型 +- **WHEN** 开发者提交信息使用不在允许列表中的类型(如 "update: 修改配置") +- **THEN** commit-msg hook MUST 以非零状态退出,提示可用类型 + +### Requirement: husky 初始化自动化 +项目 SHALL 通过 `prepare` 生命周期脚本在 `bun install` 时自动初始化 husky。 + +#### Scenario: 首次安装依赖 +- **WHEN** 开发者运行 `bun install` +- **THEN** husky SHALL 自动初始化,安装 pre-commit 和 commit-msg hooks + +#### Scenario: 已有 husky 配置时安装 +- **WHEN** 开发者运行 `bun install` 且 husky 已初始化 +- **THEN** husky 初始化 SHALL 跳过,不覆盖已有配置 diff --git a/package.json b/package.json index cfe58ed..9373d97 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,12 @@ "test": "bun test", "test:smoke": "bun run scripts/smoke.ts", "clean": "bun run scripts/clean.ts", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "prepare": "husky" }, "devDependencies": { + "@commitlint/cli": "^21.0.0", + "@commitlint/config-conventional": "^21.0.0", "@eslint/js": "^10.0.1", "@tanstack/react-query-devtools": "^5.100.10", "@types/bun": "^1.3.13", @@ -27,8 +30,13 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^10.3.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-perfectionist": "^5.9.0", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.2", + "husky": "^9.1.7", + "lint-staged": "^17.0.4", "prettier": "^3.8.3", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2", diff --git a/scripts/build.ts b/scripts/build.ts index 731d2be..3f4e72d 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -1,7 +1,7 @@ +import { $ } from "bun"; import { mkdir, readdir, rm, writeFile } from "node:fs/promises"; import { dirname, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; -import { $ } from "bun"; const buildDir = fileURLToPath(new URL("../.build/", import.meta.url)); const webDistDir = fileURLToPath(new URL("../dist/web/", import.meta.url)); @@ -9,7 +9,7 @@ const executablePath = fileURLToPath(new URL("../dist/dial-server", import.meta. const generatedAssetsPath = fileURLToPath(new URL("../.build/static-assets.ts", import.meta.url)); const generatedEntryPath = fileURLToPath(new URL("../.build/server-entry.ts", import.meta.url)); -await rm(buildDir, { recursive: true, force: true }); +await rm(buildDir, { force: true, recursive: true }); await rm(executablePath, { force: true }); await mkdir(buildDir, { recursive: true }); @@ -26,21 +26,21 @@ const assetFiles = files.filter((file) => file !== indexPath); await writeGeneratedAssets(indexPath, assetFiles); await writeGeneratedEntry(); -const target = process.env.BUN_TARGET ?? process.env.BUILD_TARGET; +const target = process.env["BUN_TARGET"] ?? process.env["BUILD_TARGET"]; const result = await Bun.build({ - entrypoints: [generatedEntryPath], compile: target ? { - target: target as Bun.Build.CompileTarget, - outfile: executablePath, - autoloadDotenv: true, autoloadBunfig: true, + autoloadDotenv: true, + outfile: executablePath, + target: target as Bun.Build.CompileTarget, } : { - outfile: executablePath, - autoloadDotenv: true, autoloadBunfig: true, + autoloadDotenv: true, + outfile: executablePath, }, + entrypoints: [generatedEntryPath], minify: true, sourcemap: "linked", }); @@ -52,7 +52,7 @@ if (!result.success) { console.log(`Built executable: ${executablePath}`); -await rm(buildDir, { recursive: true, force: true }); +await rm(buildDir, { force: true, recursive: true }); async function listFiles(directory: string): Promise { const entries = await readdir(directory, { withFileTypes: true }); @@ -71,6 +71,15 @@ async function listFiles(directory: string): Promise { return files.flat().sort((left, right) => normalize(left).localeCompare(normalize(right))); } +function normalize(path: string): string { + return path.split(sep).join("/"); +} + +function toImportPath(path: string): string { + const rel = normalize(relative(buildDir, path)); + return rel.startsWith(".") ? rel : `./${rel}`; +} + async function writeGeneratedAssets(indexPath: string, assetFiles: string[]) { const imports = [ `import type { StaticAssets } from "../src/server/app";`, @@ -133,12 +142,3 @@ void main().catch((error) => { `, ); } - -function toImportPath(path: string): string { - const rel = normalize(relative(buildDir, path)); - return rel.startsWith(".") ? rel : `./${rel}`; -} - -function normalize(path: string): string { - return path.split(sep).join("/"); -} diff --git a/scripts/clean.ts b/scripts/clean.ts index b13968a..6b8c78c 100644 --- a/scripts/clean.ts +++ b/scripts/clean.ts @@ -3,17 +3,17 @@ import { resolve } from "node:path"; const root = resolve(import.meta.dir, ".."); -const patterns: Array<{ glob: string; desc: string }> = [ - { glob: ".build/", desc: "Bun 构建缓存" }, - { glob: ".*.bun-build", desc: "Bun 构建临时文件" }, +const patterns: Array<{ desc: string; glob: string }> = [ + { desc: "Bun 构建缓存", glob: ".build/" }, + { desc: "Bun 构建临时文件", glob: ".*.bun-build" }, ]; -for (const { glob, desc } of patterns) { +for (const { desc, glob } of patterns) { const entries = await Array.fromAsync(new Bun.Glob(glob).scan({ cwd: root, dot: true })); if (entries.length === 0) continue; for (const entry of entries) { const full = resolve(root, entry); - await rm(full, { recursive: true, force: true }); + await rm(full, { force: true, recursive: true }); console.log(`已清理 ${desc}: ${entry}`); } } diff --git a/scripts/dev.ts b/scripts/dev.ts index 23e751d..0d9db15 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -7,7 +7,7 @@ const configPath = process.argv[2]; const env = { ...process.env, - BACKEND_PORT: process.env.PORT ?? "3000", + BACKEND_PORT: process.env["PORT"] ?? "3000", }; const children: ChildProcessInfo[] = [ @@ -15,16 +15,16 @@ const children: ChildProcessInfo[] = [ name: "server", process: Bun.spawn(["bun", "run", "dev:server", ...(configPath ? [configPath] : [])], { env, - stdout: "inherit", stderr: "inherit", + stdout: "inherit", }), }, { name: "web", process: Bun.spawn(["bun", "run", "dev:web"], { env, - stdout: "inherit", stderr: "inherit", + stdout: "inherit", }), }, ]; @@ -46,7 +46,7 @@ process.on("SIGTERM", () => { }); const firstExit = await Promise.race( - children.map(async (child) => ({ name: child.name, code: await child.process.exited })), + children.map(async (child) => ({ code: await child.process.exited, name: child.name })), ); stopChildren(); diff --git a/scripts/smoke.ts b/scripts/smoke.ts index 108eda3..93a8a83 100644 --- a/scripts/smoke.ts +++ b/scripts/smoke.ts @@ -1,8 +1,9 @@ -import { access } from "node:fs/promises"; -import { fileURLToPath } from "node:url"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { access } from "node:fs/promises"; import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + import type { HealthResponse, SummaryResponse } from "../src/shared/api"; const executablePath = process.argv[2] ?? fileURLToPath(new URL("../dist/dial-server", import.meta.url)); @@ -12,7 +13,7 @@ await assertExecutableExists(executablePath); const tempDir = mkdtempSync(join(tmpdir(), "dial-smoke-")); const configPath = join(tempDir, "probes.yaml"); -const port = await getFreePort(); +const port = getFreePort(); const baseUrl = `http://127.0.0.1:${port}`; writeFileSync( @@ -31,9 +32,9 @@ targets: `, ); const app = Bun.spawn([executablePath, configPath], { - stdout: "pipe", - stderr: "pipe", env: { ...process.env }, + stderr: "pipe", + stdout: "pipe", }); const stdout = readStream(app.stdout); const stderr = readStream(app.stderr); @@ -49,10 +50,10 @@ try { assert(summary.total === 1, "总览统计: total 应为 1"); assertSecurityHeaders(await fetch(`${baseUrl}/api/summary`), "/api/summary"); - const { body: targets } = await expectJson(`${baseUrl}/api/targets`, 200); + const { body: targets } = await expectJson(`${baseUrl}/api/targets`, 200); assert(Array.isArray(targets), "/api/targets 应返回数组"); assert(targets.length === 1, "/api/targets 应有 1 个目标"); - assert(targets[0].name === "httpbin", "目标名称应为 httpbin"); + assert((targets[0] as { name: string }).name === "httpbin", "目标名称应为 httpbin"); const missingApi = await fetch(`${baseUrl}/api/not-found`); assert(missingApi.status === 404, "未知 API 应返回 404"); @@ -67,7 +68,7 @@ try { const { body: fallbackHtml } = await expectText(`${baseUrl}/dashboard`, 200); assert(fallbackHtml.includes("DiAL"), "SPA fallback 未返回前端入口页面"); - const assetPath = rootHtml.match(/(?:src|href)="(\/assets\/[^"]+)"/)?.[1]; + const assetPath = /(?:src|href)="(\/assets\/[^"]+)"/.exec(rootHtml)?.[1]; assert(assetPath !== undefined, "前端入口页面未引用 /assets/* 资源"); const asset = await fetch(`${baseUrl}${assetPath}`); @@ -85,7 +86,13 @@ try { throw new Error(`executable smoke test 失败: ${message}\nstdout:\n${out}\nstderr:\n${err}`, { cause: error }); } finally { app.kill(); - rmSync(tempDir, { recursive: true, force: true }); + rmSync(tempDir, { force: true, recursive: true }); +} + +function assert(condition: boolean, message: string): asserts condition { + if (!condition) { + throw new Error(message); + } } async function assertExecutableExists(path: string) { @@ -96,37 +103,12 @@ async function assertExecutableExists(path: string) { } } -async function getFreePort(): Promise { - const server = Bun.serve({ - hostname: "127.0.0.1", - port: 0, - fetch: () => new Response("ok"), - }); - const port = server.port; - - server.stop(true); - - if (port === undefined) { - throw new Error("无法分配 smoke test 端口"); - } - - return port; -} - -async function waitForServer(url: string) { - const deadline = Date.now() + 8_000; - - while (Date.now() < deadline) { - try { - const response = await fetch(url); - - if (response.ok) return; - } catch { - await Bun.sleep(100); - } - } - - throw new Error(`服务未在超时时间内启动: ${url}`); +function assertSecurityHeaders(response: Response, label: string) { + assert(response.headers.get("x-content-type-options") === "nosniff", `${label} 缺少 nosniff 安全头`); + assert( + response.headers.get("referrer-policy") === "strict-origin-when-cross-origin", + `${label} 缺少 Referrer-Policy 安全头`, + ); } async function expectJson(url: string, status: number): Promise<{ body: T; response: Response }> { @@ -146,22 +128,41 @@ async function expectText(url: string, status: number): Promise<{ body: string; return { body: await response.text(), response }; } -function assertSecurityHeaders(response: Response, label: string) { - assert(response.headers.get("x-content-type-options") === "nosniff", `${label} 缺少 nosniff 安全头`); - assert( - response.headers.get("referrer-policy") === "strict-origin-when-cross-origin", - `${label} 缺少 Referrer-Policy 安全头`, - ); -} +function getFreePort(): number { + const server = Bun.serve({ + fetch: () => new Response("ok"), + hostname: "127.0.0.1", + port: 0, + }); + const port = server.port; -function assert(condition: boolean, message: string): asserts condition { - if (!condition) { - throw new Error(message); + void server.stop(true); + + if (port === undefined) { + throw new Error("无法分配 smoke test 端口"); } + + return port; } -async function readStream(stream: ReadableStream | null): Promise { +async function readStream(stream: null | ReadableStream): Promise { if (!stream) return ""; return new Response(stream).text(); } + +async function waitForServer(url: string) { + const deadline = Date.now() + 8_000; + + while (Date.now() < deadline) { + try { + const response = await fetch(url); + + if (response.ok) return; + } catch { + await Bun.sleep(100); + } + } + + throw new Error(`服务未在超时时间内启动: ${url}`); +} diff --git a/src/server/app.ts b/src/server/app.ts index 8db6730..0b26790 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -1,18 +1,14 @@ import type { RuntimeMode } from "../shared/api"; import type { ProbeStore } from "./checker/store"; -import { jsonResponse, createApiError } from "./helpers"; + +import { createApiError, jsonResponse } from "./helpers"; import { guardGetHead } from "./middleware"; -import { serveStaticAsset } from "./static"; import { handleHealth } from "./routes/health"; +import { handleHistory } from "./routes/history"; import { handleSummary } from "./routes/summary"; import { handleTargets } from "./routes/targets"; -import { handleHistory } from "./routes/history"; import { handleTrend } from "./routes/trend"; - -export interface StaticAssets { - indexHtml: Blob; - files: Record; -} +import { serveStaticAsset } from "./static"; export interface AppOptions { mode: RuntimeMode; @@ -20,6 +16,11 @@ export interface AppOptions { store?: ProbeStore; } +export interface StaticAssets { + files: Record; + indexHtml: Blob; +} + export function createFetchHandler(options: AppOptions) { return (request: Request): Response => { const url = new URL(request.url); @@ -45,8 +46,8 @@ export function createFetchHandler(options: AppOptions) { } return new Response("开发期请通过 Vite 前端地址访问页面。", { - status: 404, headers: { "Content-Type": "text/plain; charset=utf-8" }, + status: 404, }); }; } @@ -65,12 +66,12 @@ function handleApiRoute(url: URL, request: Request, store: ProbeStore, mode: Run return handleTargets(store, method, mode); } - const historyMatch = url.pathname.match(/^\/api\/targets\/([^/]+)\/history$/); + const historyMatch = /^\/api\/targets\/([^/]+)\/history$/.exec(url.pathname); if (historyMatch) { return handleHistory(historyMatch[1]!, url, method, store, mode); } - const trendMatch = url.pathname.match(/^\/api\/targets\/([^/]+)\/trend$/); + const trendMatch = /^\/api\/targets\/([^/]+)\/trend$/.exec(url.pathname); if (trendMatch) { return handleTrend(trendMatch[1]!, url, method, store, mode); } diff --git a/src/server/checker/config-loader.ts b/src/server/checker/config-loader.ts index 1597b50..cbd8f85 100644 --- a/src/server/checker/config-loader.ts +++ b/src/server/checker/config-loader.ts @@ -1,11 +1,7 @@ -import type { - DefaultsConfig, - ProbeConfig, - ResolvedTarget, - EngineRuntimeConfig, - TargetConfig, -} from "./types"; import { dirname, resolve } from "node:path"; + +import type { DefaultsConfig, EngineRuntimeConfig, ProbeConfig, ResolvedTarget, TargetConfig } from "./types"; + import { checkerRegistry } from "./runner"; const DEFAULT_HOST = "127.0.0.1"; @@ -16,11 +12,11 @@ const DEFAULT_TIMEOUT = "10s"; const DEFAULT_MAX_CONCURRENT_CHECKS = 20; export interface ResolvedConfig { - host: string; - port: number; - dataDir: string; configDir: string; + dataDir: string; + host: string; maxConcurrentChecks: number; + port: number; targets: ResolvedTarget[]; } @@ -32,7 +28,7 @@ export async function loadConfig(configPath: string): Promise { } const content = await file.text(); - const raw = Bun.YAML.parse(content) as ProbeConfig | null; + const raw = Bun.YAML.parse(content) as null | ProbeConfig; if (!raw) { throw new Error("配置文件内容为空或格式无效"); @@ -61,21 +57,7 @@ export async function loadConfig(configPath: string): Promise { resolveTarget(target, defaults, defaultIntervalMs, defaultTimeoutMs, configDir), ); - return { host, port, dataDir, configDir, maxConcurrentChecks, targets }; -} - -function validateRuntime(runtime: EngineRuntimeConfig): number { - if (runtime.maxConcurrentChecks === undefined) return DEFAULT_MAX_CONCURRENT_CHECKS; - - if ( - typeof runtime.maxConcurrentChecks !== "number" || - !Number.isInteger(runtime.maxConcurrentChecks) || - runtime.maxConcurrentChecks <= 0 - ) { - throw new Error("runtime.maxConcurrentChecks 必须为正整数"); - } - - return runtime.maxConcurrentChecks; + return { configDir, dataDir, host, maxConcurrentChecks, port, targets }; } function resolveTarget( @@ -89,7 +71,7 @@ function resolveTarget( const timeoutMs = parseDuration(target.timeout ?? defaults.timeout ?? DEFAULT_TIMEOUT); const checker = checkerRegistry.get(target.type); - const result = checker.resolve(target, { defaults, configDir, defaultIntervalMs, defaultTimeoutMs }); + const result = checker.resolve(target, { configDir, defaultIntervalMs, defaults, defaultTimeoutMs }); result.intervalMs = intervalMs; result.timeoutMs = timeoutMs; @@ -109,7 +91,7 @@ function validateConfig(config: ProbeConfig): void { const raw = config.targets[i] as unknown as Record; const name = raw["name"]; - if (!name || typeof name !== "string" || (name as string).trim() === "") { + if (!name || typeof name !== "string" || name.trim() === "") { throw new Error(`第 ${i + 1} 个 target 缺少 name 字段`); } @@ -127,14 +109,28 @@ function validateConfig(config: ProbeConfig): void { throw new Error(`target "${name}" 的 group 字段必须为字符串`); } - if (names.has(name as string)) { + if (names.has(name)) { throw new Error(`target name 重复: "${name}"`); } - names.add(name as string); + names.add(name); } } +function validateRuntime(runtime: EngineRuntimeConfig): number { + if (runtime.maxConcurrentChecks === undefined) return DEFAULT_MAX_CONCURRENT_CHECKS; + + if ( + typeof runtime.maxConcurrentChecks !== "number" || + !Number.isInteger(runtime.maxConcurrentChecks) || + runtime.maxConcurrentChecks <= 0 + ) { + throw new Error("runtime.maxConcurrentChecks 必须为正整数"); + } + + return runtime.maxConcurrentChecks; +} + const DURATION_REGEX = /^(\d+(?:\.\d+)?)(ms|s|m)$/; export function parseDuration(value: string): number { diff --git a/src/server/checker/engine.ts b/src/server/checker/engine.ts index 6056873..c79fb49 100644 --- a/src/server/checker/engine.ts +++ b/src/server/checker/engine.ts @@ -1,14 +1,16 @@ -import type { CheckResult, ResolvedTarget } from "./types"; -import type { ProbeStore } from "./store"; -import { checkerRegistry } from "./runner"; import { groupBy, Semaphore } from "es-toolkit"; +import type { ProbeStore } from "./store"; +import type { CheckResult, ResolvedTarget } from "./types"; + +import { checkerRegistry } from "./runner"; + export class ProbeEngine { - private timers: ReturnType[] = []; - private store: ProbeStore; - private targets: ResolvedTarget[]; - private targetNameToId: Map = new Map(); private semaphore: Semaphore; + private store: ProbeStore; + private targetNameToId = new Map(); + private targets: ResolvedTarget[]; + private timers: Array> = []; constructor(store: ProbeStore, targets: ResolvedTarget[], maxConcurrentChecks?: number) { this.store = store; @@ -59,6 +61,13 @@ export class ProbeEngine { } } + private refreshCache(): void { + this.targetNameToId.clear(); + for (const target of this.store.getTargets()) { + this.targetNameToId.set(target.name, target.id); + } + } + private async runCheck(target: ResolvedTarget): Promise { const checker = checkerRegistry.get(target.type); const controller = new AbortController(); @@ -76,19 +85,12 @@ export class ProbeEngine { if (!targetId) return; this.store.insertCheckResult({ + durationMs: result.durationMs, + failure: result.failure, + matched: result.matched, + statusDetail: result.statusDetail, targetId, timestamp: result.timestamp, - matched: result.matched, - durationMs: result.durationMs, - statusDetail: result.statusDetail, - failure: result.failure, }); } - - private refreshCache(): void { - this.targetNameToId.clear(); - for (const target of this.store.getTargets()) { - this.targetNameToId.set(target.name, target.id); - } - } } diff --git a/src/server/checker/runner/command/expect.ts b/src/server/checker/runner/command/expect.ts index f40df9f..8391617 100644 --- a/src/server/checker/runner/command/expect.ts +++ b/src/server/checker/runner/command/expect.ts @@ -1,18 +1,19 @@ -import { mismatchFailure } from "../shared/failure"; import type { ExpectResult } from "../shared/duration"; +import { mismatchFailure } from "../shared/failure"; + export function checkExitCode(exitCode: number, allowed: number[]): ExpectResult { if (!allowed.includes(exitCode)) { return { - matched: false, failure: mismatchFailure( "exitCode", "exitCode", allowed, exitCode, - `exitCode ${exitCode} not in [${allowed}]`, + `exitCode ${exitCode} not in [${allowed.join(", ")}]`, ), + matched: false, }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } diff --git a/src/server/checker/runner/command/runner.ts b/src/server/checker/runner/command/runner.ts index 9b4a9a4..e47ae0b 100644 --- a/src/server/checker/runner/command/runner.ts +++ b/src/server/checker/runner/command/runner.ts @@ -1,26 +1,227 @@ import { isError } from "es-toolkit"; -import type { CheckResult } from "../../types"; -import type { Checker, CheckerContext, ResolveContext } from "../types"; +import { resolve } from "node:path"; + import type { - CommandExpectConfig, + CheckResult, CommandTargetConfig, ResolvedCommandTarget, ResolvedTarget, TargetConfig, } from "../../types"; +import type { Checker, CheckerContext, ResolveContext } from "../types"; + import { parseSize } from "../../size"; -import { checkExitCode } from "./expect"; import { checkDuration } from "../shared/duration"; -import { checkTextRules } from "../shared/text"; import { errorFailure } from "../shared/failure"; -import { resolve } from "node:path"; +import { checkTextRules } from "../shared/text"; +import { checkExitCode } from "./expect"; + +export class CommandChecker implements Checker { + readonly type = "command"; + + async execute(target: ResolvedTarget, ctx: CheckerContext): Promise { + const t = target as ResolvedCommandTarget; + const timestamp = new Date().toISOString(); + const start = performance.now(); + + let proc: ReturnType; + + try { + proc = Bun.spawn([t.command.exec, ...t.command.args], { + cwd: t.command.cwd, + env: t.command.env, + stderr: "pipe", + stdin: "ignore", + stdout: "pipe", + }); + } catch (error) { + const durationMs = Math.round(performance.now() - start); + return { + durationMs, + failure: errorFailure("exitCode", "spawn", isError(error) ? error.message : String(error)), + matched: false, + statusDetail: null, + targetName: t.name, + timestamp, + }; + } + + ctx.signal.addEventListener( + "abort", + () => { + try { + proc.kill(); + } catch { + /* best-effort kill */ + } + }, + { once: true }, + ); + + let outputResult: { exceeded: boolean; stderr: string; stdout: string }; + + try { + outputResult = await readOutput( + proc.stdout as ReadableStream, + proc.stderr as ReadableStream, + () => proc.kill(), + t.command.maxOutputBytes, + ); + } catch { + const durationMs = Math.round(performance.now() - start); + return { + durationMs, + failure: errorFailure("exitCode", "execution", "输出读取失败"), + matched: false, + statusDetail: null, + targetName: t.name, + timestamp, + }; + } + + await proc.exited; + + const durationMs = Math.round(performance.now() - start); + const exitCode = proc.exitCode ?? 1; + + if (outputResult.exceeded) { + return { + durationMs, + failure: errorFailure("exitCode", "output", `输出超过限制 ${t.command.maxOutputBytes} 字节`), + matched: false, + statusDetail: `exitCode=${exitCode}`, + targetName: t.name, + timestamp, + }; + } + + if (ctx.signal.aborted) { + return { + durationMs, + failure: errorFailure("exitCode", "timeout", `命令执行超时 (${t.timeoutMs}ms)`), + matched: false, + statusDetail: null, + targetName: t.name, + timestamp, + }; + } + + const exitCodeResult = checkExitCode(exitCode, t.expect?.exitCode ?? [0]); + if (!exitCodeResult.matched) { + return { + durationMs, + failure: exitCodeResult.failure, + matched: false, + statusDetail: `exitCode=${exitCode}`, + targetName: t.name, + timestamp, + }; + } + + const durationResult = checkDuration(durationMs, t.expect?.maxDurationMs); + if (!durationResult.matched) { + return { + durationMs, + failure: durationResult.failure, + matched: false, + statusDetail: `exitCode=${exitCode}`, + targetName: t.name, + timestamp, + }; + } + + if (t.expect?.stdout && t.expect.stdout.length > 0) { + const stdoutResult = checkTextRules(outputResult.stdout, t.expect.stdout, "stdout"); + if (!stdoutResult.matched) { + return { + durationMs, + failure: stdoutResult.failure, + matched: false, + statusDetail: `exitCode=${exitCode}`, + targetName: t.name, + timestamp, + }; + } + } + + if (t.expect?.stderr && t.expect.stderr.length > 0) { + const stderrResult = checkTextRules(outputResult.stderr, t.expect.stderr, "stderr"); + if (!stderrResult.matched) { + return { + durationMs, + failure: stderrResult.failure, + matched: false, + statusDetail: `exitCode=${exitCode}`, + targetName: t.name, + timestamp, + }; + } + } + + return { + durationMs, + failure: null, + matched: true, + statusDetail: `exitCode=${exitCode}`, + targetName: t.name, + timestamp, + }; + } + + resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget { + const t = target as TargetConfig & { command: CommandTargetConfig; type: "command" }; + const commandDefaults = context.defaults.command; + + if (!t.command.exec || t.command.exec.trim() === "") { + throw new Error(`target "${t.name}" 缺少 command.exec 字段`); + } + + const cwd = t.command.cwd ?? commandDefaults?.cwd ?? "."; + const resolvedCwd = resolve(context.configDir, cwd); + + const maxOutputBytes = parseSize(t.command.maxOutputBytes ?? commandDefaults?.maxOutputBytes ?? "100MB"); + + const env = { ...process.env, ...(t.command.env ?? {}) } as Record; + + return { + command: { + args: t.command.args ?? [], + cwd: resolvedCwd, + env, + exec: t.command.exec, + maxOutputBytes, + }, + expect: target.expect, + group: target.group ?? "default", + intervalMs: context.defaultIntervalMs, + name: t.name, + timeoutMs: context.defaultTimeoutMs, + type: "command", + } satisfies ResolvedCommandTarget; + } + + serialize(target: ResolvedTarget): { config: string; target: string } { + const t = target as ResolvedCommandTarget; + const parts = [t.command.exec, ...t.command.args]; + return { + config: JSON.stringify({ + args: t.command.args, + cwd: t.command.cwd, + env: t.command.env, + exec: t.command.exec, + maxOutputBytes: t.command.maxOutputBytes, + }), + target: `exec ${parts.join(" ")}`, + }; + } +} async function readOutput( stdout: ReadableStream, stderr: ReadableStream, kill: () => void, maxBytes: number, -): Promise<{ stdout: string; stderr: string; exceeded: boolean }> { +): Promise<{ exceeded: boolean; stderr: string; stdout: string }> { let totalBytes = 0; let exceeded = false; let killed = false; @@ -61,203 +262,5 @@ async function readOutput( const [out, err] = await Promise.all([readStream(stdout), readStream(stderr)]); - return { stdout: out, stderr: err, exceeded }; -} - -export class CommandChecker implements Checker { - readonly type = "command"; - - resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget { - const t = target as TargetConfig & { type: "command"; command: CommandTargetConfig }; - const commandDefaults = context.defaults.command; - - if (!t.command.exec || t.command.exec.trim() === "") { - throw new Error(`target "${t.name}" 缺少 command.exec 字段`); - } - - const cwd = t.command.cwd ?? commandDefaults?.cwd ?? "."; - const resolvedCwd = resolve(context.configDir, cwd); - - const maxOutputBytes = parseSize( - t.command.maxOutputBytes ?? commandDefaults?.maxOutputBytes ?? "100MB", - ); - - const env = { ...process.env, ...(t.command.env ?? {}) } as Record; - - return { - type: "command", - name: t.name, - group: target.group ?? "default", - command: { - exec: t.command.exec, - args: t.command.args ?? [], - cwd: resolvedCwd, - env, - maxOutputBytes, - }, - intervalMs: context.defaultIntervalMs, - timeoutMs: context.defaultTimeoutMs, - expect: target.expect as CommandExpectConfig | undefined, - } satisfies ResolvedCommandTarget; - } - - async execute(target: ResolvedTarget, ctx: CheckerContext): Promise { - const t = target as ResolvedCommandTarget; - const timestamp = new Date().toISOString(); - const start = performance.now(); - - let proc: ReturnType; - - try { - proc = Bun.spawn([t.command.exec, ...t.command.args], { - cwd: t.command.cwd, - env: t.command.env, - stdin: "ignore", - stdout: "pipe", - stderr: "pipe", - }); - } catch (error) { - const durationMs = Math.round(performance.now() - start); - return { - targetName: t.name, - timestamp, - matched: false, - durationMs, - statusDetail: null, - failure: errorFailure("exitCode", "spawn", isError(error) ? error.message : String(error)), - }; - } - - ctx.signal.addEventListener("abort", () => { - try { - proc.kill(); - } catch { - /* best-effort kill */ - } - }, { once: true }); - - let outputResult: { stdout: string; stderr: string; exceeded: boolean }; - - try { - outputResult = await readOutput( - proc.stdout as ReadableStream, - proc.stderr as ReadableStream, - () => proc.kill(), - t.command.maxOutputBytes, - ); - } catch { - const durationMs = Math.round(performance.now() - start); - return { - targetName: t.name, - timestamp, - matched: false, - durationMs, - statusDetail: null, - failure: errorFailure("exitCode", "execution", "输出读取失败"), - }; - } - - await proc.exited; - - const durationMs = Math.round(performance.now() - start); - const exitCode = proc.exitCode ?? 1; - - if (outputResult.exceeded) { - return { - targetName: t.name, - timestamp, - matched: false, - durationMs, - statusDetail: `exitCode=${exitCode}`, - failure: errorFailure("exitCode", "output", `输出超过限制 ${t.command.maxOutputBytes} 字节`), - }; - } - - if (ctx.signal.aborted) { - return { - targetName: t.name, - timestamp, - matched: false, - durationMs, - statusDetail: null, - failure: errorFailure("exitCode", "timeout", `命令执行超时 (${t.timeoutMs}ms)`), - }; - } - - const exitCodeResult = checkExitCode(exitCode, t.expect?.exitCode ?? [0]); - if (!exitCodeResult.matched) { - return { - targetName: t.name, - timestamp, - matched: false, - durationMs, - statusDetail: `exitCode=${exitCode}`, - failure: exitCodeResult.failure, - }; - } - - const durationResult = checkDuration(durationMs, t.expect?.maxDurationMs); - if (!durationResult.matched) { - return { - targetName: t.name, - timestamp, - matched: false, - durationMs, - statusDetail: `exitCode=${exitCode}`, - failure: durationResult.failure, - }; - } - - if (t.expect?.stdout && t.expect.stdout.length > 0) { - const stdoutResult = checkTextRules(outputResult.stdout, t.expect.stdout, "stdout"); - if (!stdoutResult.matched) { - return { - targetName: t.name, - timestamp, - matched: false, - durationMs, - statusDetail: `exitCode=${exitCode}`, - failure: stdoutResult.failure, - }; - } - } - - if (t.expect?.stderr && t.expect.stderr.length > 0) { - const stderrResult = checkTextRules(outputResult.stderr, t.expect.stderr, "stderr"); - if (!stderrResult.matched) { - return { - targetName: t.name, - timestamp, - matched: false, - durationMs, - statusDetail: `exitCode=${exitCode}`, - failure: stderrResult.failure, - }; - } - } - - return { - targetName: t.name, - timestamp, - matched: true, - durationMs, - statusDetail: `exitCode=${exitCode}`, - failure: null, - }; - } - - serialize(target: ResolvedTarget): { target: string; config: string } { - const t = target as ResolvedCommandTarget; - const parts = [t.command.exec, ...t.command.args]; - return { - target: `exec ${parts.join(" ")}`, - config: JSON.stringify({ - exec: t.command.exec, - args: t.command.args, - cwd: t.command.cwd, - env: t.command.env, - maxOutputBytes: t.command.maxOutputBytes, - }), - }; - } + return { exceeded, stderr: err, stdout: out }; } diff --git a/src/server/checker/runner/http/expect.ts b/src/server/checker/runner/http/expect.ts index 7c694e5..a98a634 100644 --- a/src/server/checker/runner/http/expect.ts +++ b/src/server/checker/runner/http/expect.ts @@ -1,31 +1,16 @@ import type { HeaderExpect, HttpExpectConfig } from "../../types"; -import { mismatchFailure, errorFailure } from "../shared/failure"; -import { applyOperator } from "../shared/operator"; -import { checkDuration } from "../shared/duration"; -import { checkBodyExpect } from "../shared/body"; import type { ExpectResult } from "../shared/duration"; -export function checkStatus(statusCode: number, allowed: number[]): ExpectResult { - if (!allowed.includes(statusCode)) { - return { - matched: false, - failure: mismatchFailure( - "status", - "status", - allowed, - statusCode, - `status ${statusCode} not in [${allowed}]`, - ), - }; - } - return { matched: true, failure: null }; -} +import { checkBodyExpect } from "../shared/body"; +import { checkDuration } from "../shared/duration"; +import { errorFailure, mismatchFailure } from "../shared/failure"; +import { applyOperator } from "../shared/operator"; export function checkHeaders( headers: Record, headerExpects?: Record, ): ExpectResult { - if (!headerExpects) return { matched: true, failure: null }; + if (!headerExpects) return { failure: null, matched: true }; for (const [key, expected] of Object.entries(headerExpects)) { const actualValue = headers[key.toLowerCase()]; @@ -34,36 +19,36 @@ export function checkHeaders( if (typeof expected === "string") { if (actualValue !== expected) { return { - matched: false, failure: mismatchFailure("headers", path, expected, actualValue, `header ${key} mismatch`), + matched: false, }; } } else { if (actualValue === undefined) { if (expected.exists !== false) { return { - matched: false, failure: mismatchFailure("headers", path, "defined", undefined, `header ${key} not found`), + matched: false, }; } continue; } if (!applyOperator(actualValue, expected)) { return { - matched: false, failure: mismatchFailure("headers", path, expected, actualValue, `header ${key} mismatch`), + matched: false, }; } } } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } export function checkHttpExpect( statusCode: number, headers: Record, - body: string | null, + body: null | string, durationMs: number, expect?: HttpExpectConfig, ): ExpectResult { @@ -83,13 +68,29 @@ export function checkHttpExpect( if (expect.body && expect.body.length > 0) { if (body === null) { return { - matched: false, failure: errorFailure("body", "body", "body is null but body rules are configured"), + matched: false, }; } const bodyResult = checkBodyExpect(body, expect.body); if (!bodyResult.matched) return bodyResult; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; +} + +export function checkStatus(statusCode: number, allowed: number[]): ExpectResult { + if (!allowed.includes(statusCode)) { + return { + failure: mismatchFailure( + "status", + "status", + allowed, + statusCode, + `status ${statusCode} not in [${allowed.join(", ")}]`, + ), + matched: false, + }; + } + return { failure: null, matched: true }; } diff --git a/src/server/checker/runner/http/runner.ts b/src/server/checker/runner/http/runner.ts index 1700418..ea63954 100644 --- a/src/server/checker/runner/http/runner.ts +++ b/src/server/checker/runner/http/runner.ts @@ -1,51 +1,15 @@ -import type { CheckResult } from "../../types"; import { isError } from "es-toolkit"; -import type { - Checker, - CheckerContext, - ResolveContext, -} from "../types"; -import type { - HttpExpectConfig, - HttpTargetConfig, - ResolvedHttpTarget, - ResolvedTarget, - TargetConfig, -} from "../../types"; + +import type { CheckResult, HttpTargetConfig, ResolvedHttpTarget, ResolvedTarget, TargetConfig } from "../../types"; +import type { Checker, CheckerContext, ResolveContext } from "../types"; + import { parseSize } from "../../size"; -import { checkHttpExpect } from "./expect"; import { errorFailure } from "../shared/failure"; +import { checkHttpExpect } from "./expect"; export class HttpChecker implements Checker { readonly type = "http"; - resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget { - const t = target as TargetConfig & { type: "http"; http: HttpTargetConfig }; - const httpDefaults = context.defaults.http; - - if (!t.http.url || t.http.url.trim() === "") { - throw new Error(`target "${t.name}" 缺少 http.url 字段`); - } - - const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? httpDefaults?.maxBodyBytes ?? "100MB"); - - return { - type: "http", - name: t.name, - group: target.group ?? "default", - http: { - url: t.http.url, - method: t.http.method ?? httpDefaults?.method ?? "GET", - headers: { ...(httpDefaults?.headers ?? {}), ...(t.http.headers ?? {}) }, - body: t.http.body, - maxBodyBytes, - }, - intervalMs: context.defaultIntervalMs, - timeoutMs: context.defaultTimeoutMs, - expect: target.expect as HttpExpectConfig | undefined, - } satisfies ResolvedHttpTarget; - } - async execute(target: ResolvedTarget, ctx: CheckerContext): Promise { const t = target as ResolvedHttpTarget; const timestamp = new Date().toISOString(); @@ -54,9 +18,9 @@ export class HttpChecker implements Checker { const start = performance.now(); const response = await fetch(t.http.url, { - method: t.http.method, - headers: t.http.headers, body: t.http.method !== "GET" && t.http.method !== "HEAD" ? t.http.body : undefined, + headers: t.http.headers, + method: t.http.method, signal: ctx.signal, }); @@ -67,19 +31,19 @@ export class HttpChecker implements Checker { const hasBodyRules = !!(t.expect?.body && t.expect.body.length > 0); const preBodyExpect = t.expect - ? { status: t.expect.status, maxDurationMs: t.expect.maxDurationMs, headers: t.expect.headers } + ? { headers: t.expect.headers, maxDurationMs: t.expect.maxDurationMs, status: t.expect.status } : undefined; const preBodyResult = checkHttpExpect(statusCode, responseHeaders, null, durationMs, preBodyExpect); if (!hasBodyRules || !preBodyResult.matched) { return { + durationMs, + failure: preBodyResult.failure, + matched: preBodyResult.matched, + statusDetail: `HTTP ${statusCode}`, targetName: t.name, timestamp, - matched: preBodyResult.matched, - durationMs, - statusDetail: `HTTP ${statusCode}`, - failure: preBodyResult.failure, }; } @@ -87,16 +51,12 @@ export class HttpChecker implements Checker { if (bodyBuffer.byteLength > t.http.maxBodyBytes) { return { + durationMs, + failure: errorFailure("body", "body", `响应体大小 ${bodyBuffer.byteLength} 超过限制 ${t.http.maxBodyBytes}`), + matched: false, + statusDetail: `HTTP ${statusCode}`, targetName: t.name, timestamp, - matched: false, - durationMs, - statusDetail: `HTTP ${statusCode}`, - failure: errorFailure( - "body", - "body", - `响应体大小 ${bodyBuffer.byteLength} 超过限制 ${t.http.maxBodyBytes}`, - ), }; } @@ -104,42 +64,69 @@ export class HttpChecker implements Checker { const fullResult = checkHttpExpect(statusCode, responseHeaders, body, durationMs, t.expect); return { + durationMs, + failure: fullResult.failure, + matched: fullResult.matched, + statusDetail: `HTTP ${statusCode}`, targetName: t.name, timestamp, - matched: fullResult.matched, - durationMs, - statusDetail: `HTTP ${statusCode}`, - failure: fullResult.failure, }; } catch (error) { const isTimeout = ctx.signal.aborted || (error instanceof DOMException && error.name === "AbortError"); return { - targetName: t.name, - timestamp, - matched: false, durationMs: null, - statusDetail: null, failure: errorFailure( "status", "request", isTimeout ? `请求超时 (${t.timeoutMs}ms)` : isError(error) ? error.message : String(error), ), + matched: false, + statusDetail: null, + targetName: t.name, + timestamp, }; } } - serialize(target: ResolvedTarget): { target: string; config: string } { + resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget { + const t = target as TargetConfig & { http: HttpTargetConfig; type: "http" }; + const httpDefaults = context.defaults.http; + + if (!t.http.url || t.http.url.trim() === "") { + throw new Error(`target "${t.name}" 缺少 http.url 字段`); + } + + const maxBodyBytes = parseSize(t.http.maxBodyBytes ?? httpDefaults?.maxBodyBytes ?? "100MB"); + + return { + expect: target.expect, + group: target.group ?? "default", + http: { + body: t.http.body, + headers: { ...(httpDefaults?.headers ?? {}), ...(t.http.headers ?? {}) }, + maxBodyBytes, + method: t.http.method ?? httpDefaults?.method ?? "GET", + url: t.http.url, + }, + intervalMs: context.defaultIntervalMs, + name: t.name, + timeoutMs: context.defaultTimeoutMs, + type: "http", + } satisfies ResolvedHttpTarget; + } + + serialize(target: ResolvedTarget): { config: string; target: string } { const t = target as ResolvedHttpTarget; return { - target: t.http.url, config: JSON.stringify({ - url: t.http.url, - method: t.http.method, - headers: t.http.headers, body: t.http.body, + headers: t.http.headers, maxBodyBytes: t.http.maxBodyBytes, + method: t.http.method, + url: t.http.url, }), + target: t.http.url, }; } } diff --git a/src/server/checker/runner/index.ts b/src/server/checker/runner/index.ts index 6fcdb52..bd96794 100644 --- a/src/server/checker/runner/index.ts +++ b/src/server/checker/runner/index.ts @@ -1,6 +1,6 @@ -import { checkerRegistry } from "./registry"; -import { HttpChecker } from "./http/runner"; import { CommandChecker } from "./command/runner"; +import { HttpChecker } from "./http/runner"; +import { checkerRegistry } from "./registry"; export function registerCheckers(): void { checkerRegistry.register(new HttpChecker()); diff --git a/src/server/checker/runner/registry.ts b/src/server/checker/runner/registry.ts index fe32d84..30ba135 100644 --- a/src/server/checker/runner/registry.ts +++ b/src/server/checker/runner/registry.ts @@ -1,15 +1,12 @@ import type { Checker } from "./types"; export class CheckerRegistry { - private checkers = new Map(); - - register(checker: Checker): void { - if (this.checkers.has(checker.type)) { - throw new Error(`Checker type "${checker.type}" 已注册`); - } - this.checkers.set(checker.type, checker); + get supportedTypes(): string[] { + return [...this.checkers.keys()]; } + private checkers = new Map(); + get(type: string): Checker { const checker = this.checkers.get(type); if (!checker) { @@ -18,8 +15,11 @@ export class CheckerRegistry { return checker; } - get supportedTypes(): string[] { - return [...this.checkers.keys()]; + register(checker: Checker): void { + if (this.checkers.has(checker.type)) { + throw new Error(`Checker type "${checker.type}" 已注册`); + } + this.checkers.set(checker.type, checker); } } diff --git a/src/server/checker/runner/shared/body.ts b/src/server/checker/runner/shared/body.ts index fa3e1c7..967caee 100644 --- a/src/server/checker/runner/shared/body.ts +++ b/src/server/checker/runner/shared/body.ts @@ -1,58 +1,26 @@ -import type { BodyRule, CssRule, JsonRule, XpathRule } from "../../types"; +import { DOMParser } from "@xmldom/xmldom"; import * as cheerio from "cheerio"; import * as xpath from "xpath"; -import { DOMParser } from "@xmldom/xmldom"; -import { mismatchFailure, errorFailure } from "./failure"; -import { applyOperator, evaluateJsonPath } from "./operator"; + +import type { BodyRule, CssRule, JsonRule, XpathRule } from "../../types"; import type { ExpectResult } from "./duration"; -function checkJsonRule( - body: string, - rule: JsonRule, - rulePath: string, -): ExpectResult { - const { path, ...operators } = rule; - const fullPath = `${rulePath}.json(${path})`; +import { errorFailure, mismatchFailure } from "./failure"; +import { applyOperator, evaluateJsonPath } from "./operator"; - let json: unknown; - try { - json = JSON.parse(body); - } catch { - return { - matched: false, - failure: errorFailure("body", fullPath, "body is not valid JSON"), - }; +export function checkBodyExpect(body: string, rules?: BodyRule[]): ExpectResult { + if (!rules || rules.length === 0) return { failure: null, matched: true }; + + for (let i = 0; i < rules.length; i++) { + const result = checkSingleBodyRule(body, rules[i]!, i); + if (!result.matched) return result; } - const actual = evaluateJsonPath(json, path); - const opKeys = Object.keys(operators); - - if (opKeys.length === 0) { - if (actual === undefined) { - return { - matched: false, - failure: mismatchFailure("body", fullPath, "defined", actual, `path ${path} is undefined`), - }; - } - return { matched: true, failure: null }; - } - - const matched = applyOperator(actual, operators); - if (!matched) { - return { - matched: false, - failure: mismatchFailure("body", fullPath, operators, actual, `json path ${path} mismatch`), - }; - } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } -function checkCssRule( - body: string, - rule: CssRule, - rulePath: string, -): ExpectResult { - const { selector, attr, ...operators } = rule; +function checkCssRule(body: string, rule: CssRule, rulePath: string): ExpectResult { + const { attr, selector, ...operators } = rule; const fullPath = `${rulePath}.css(${selector}${attr ? `@${attr}` : ""})`; let $: cheerio.CheerioAPI; @@ -60,8 +28,8 @@ function checkCssRule( $ = cheerio.load(body); } catch { return { - matched: false, failure: errorFailure("body", fullPath, "failed to parse HTML"), + matched: false, }; } @@ -72,44 +40,44 @@ function checkCssRule( if (attr !== undefined) { if (el.attr(attr) === undefined) { return { - matched: false, failure: mismatchFailure("body", fullPath, "attr present", undefined, `attribute ${attr} not found`), + matched: false, }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } if (el.length === 0) { return { - matched: false, failure: mismatchFailure("body", fullPath, "element found", "no match", `selector ${selector} not found`), + matched: false, }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } if (operators.exists === true) { if (el.length === 0) { return { - matched: false, failure: mismatchFailure("body", fullPath, true, false, `selector ${selector} not found`), + matched: false, }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } if (operators.exists === false) { if (el.length > 0) { return { - matched: false, failure: mismatchFailure("body", fullPath, false, true, `selector ${selector} exists`), + matched: false, }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } if (el.length === 0) { return { - matched: false, failure: mismatchFailure("body", fullPath, "element found", "no match", `selector ${selector} not found`), + matched: false, }; } @@ -117,84 +85,73 @@ function checkCssRule( const matched = applyOperator(actual ?? "", operators); if (!matched) { return { - matched: false, failure: mismatchFailure("body", fullPath, operators, actual, `css selector ${selector} mismatch`), + matched: false, }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } -function checkXpathRule( - body: string, - rule: XpathRule, - rulePath: string, -): ExpectResult { +function checkJsonRule(body: string, rule: JsonRule, rulePath: string): ExpectResult { const { path, ...operators } = rule; - const fullPath = `${rulePath}.xpath(${path})`; + const fullPath = `${rulePath}.json(${path})`; - let doc: ReturnType; + let json: unknown; try { - doc = new DOMParser().parseFromString(body, "text/xml"); + json = JSON.parse(body); } catch { return { + failure: errorFailure("body", fullPath, "body is not valid JSON"), matched: false, - failure: errorFailure("body", fullPath, "failed to parse XML/HTML"), }; } - const nodes = xpath.select(path, doc as unknown as Node); - if (!nodes || !Array.isArray(nodes) || nodes.length === 0) { - return { - matched: false, - failure: mismatchFailure("body", fullPath, "node found", "no match", `xpath ${path} not found`), - }; - } - - const node = nodes[0]!; - const actual = node.nodeValue ?? (node as unknown as Element).textContent ?? ""; + const actual = evaluateJsonPath(json, path); const opKeys = Object.keys(operators); if (opKeys.length === 0) { - return { matched: true, failure: null }; + if (actual === undefined) { + return { + failure: mismatchFailure("body", fullPath, "defined", actual, `path ${path} is undefined`), + matched: false, + }; + } + return { failure: null, matched: true }; } const matched = applyOperator(actual, operators); if (!matched) { return { + failure: mismatchFailure("body", fullPath, operators, actual, `json path ${path} mismatch`), matched: false, - failure: mismatchFailure("body", fullPath, operators, actual, `xpath ${path} mismatch`), }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } -function checkSingleBodyRule( - body: string, - rule: BodyRule, - index: number, -): ExpectResult { +function checkSingleBodyRule(body: string, rule: BodyRule, index: number): ExpectResult { const rulePath = `body[${index}]`; if ("contains" in rule) { const matched = body.includes(rule.contains); if (!matched) { return { - matched: false, failure: mismatchFailure("body", rulePath, rule.contains, body, `body does not contain "${rule.contains}"`), + matched: false, }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } if ("regex" in rule) { const matched = new RegExp(rule.regex).test(body); if (!matched) { return { - matched: false, failure: mismatchFailure("body", rulePath, `/${rule.regex}/`, body, `body does not match /${rule.regex}/`), + matched: false, }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } if ("json" in rule) { @@ -209,16 +166,45 @@ function checkSingleBodyRule( return checkXpathRule(body, rule.xpath, rulePath); } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } -export function checkBodyExpect(body: string, rules?: BodyRule[]): ExpectResult { - if (!rules || rules.length === 0) return { matched: true, failure: null }; +function checkXpathRule(body: string, rule: XpathRule, rulePath: string): ExpectResult { + const { path, ...operators } = rule; + const fullPath = `${rulePath}.xpath(${path})`; - for (let i = 0; i < rules.length; i++) { - const result = checkSingleBodyRule(body, rules[i]!, i); - if (!result.matched) return result; + let doc: ReturnType; + try { + doc = new DOMParser().parseFromString(body, "text/xml"); + } catch { + return { + failure: errorFailure("body", fullPath, "failed to parse XML/HTML"), + matched: false, + }; } - return { matched: true, failure: null }; + const nodes = xpath.select(path, doc as unknown as Node); + if (!nodes || !Array.isArray(nodes) || nodes.length === 0) { + return { + failure: mismatchFailure("body", fullPath, "node found", "no match", `xpath ${path} not found`), + matched: false, + }; + } + + const node = nodes[0]!; + const actual = node.nodeValue ?? (node as unknown as Element).textContent ?? ""; + const opKeys = Object.keys(operators); + + if (opKeys.length === 0) { + return { failure: null, matched: true }; + } + + const matched = applyOperator(actual, operators); + if (!matched) { + return { + failure: mismatchFailure("body", fullPath, operators, actual, `xpath ${path} mismatch`), + matched: false, + }; + } + return { failure: null, matched: true }; } diff --git a/src/server/checker/runner/shared/duration.ts b/src/server/checker/runner/shared/duration.ts index db19e05..f1e860f 100644 --- a/src/server/checker/runner/shared/duration.ts +++ b/src/server/checker/runner/shared/duration.ts @@ -1,16 +1,16 @@ import type { CheckFailure } from "../../types"; + import { mismatchFailure } from "./failure"; export interface ExpectResult { - matched: boolean; failure: CheckFailure | null; + matched: boolean; } export function checkDuration(durationMs: number, maxDurationMs?: number): ExpectResult { - if (maxDurationMs === undefined) return { matched: true, failure: null }; + if (maxDurationMs === undefined) return { failure: null, matched: true }; if (durationMs > maxDurationMs) { return { - matched: false, failure: mismatchFailure( "duration", "duration", @@ -18,7 +18,8 @@ export function checkDuration(durationMs: number, maxDurationMs?: number): Expec durationMs, `duration ${durationMs}ms > ${maxDurationMs}ms`, ), + matched: false, }; } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } diff --git a/src/server/checker/runner/shared/failure.ts b/src/server/checker/runner/shared/failure.ts index 884bfbc..78052d2 100644 --- a/src/server/checker/runner/shared/failure.ts +++ b/src/server/checker/runner/shared/failure.ts @@ -1,10 +1,12 @@ import type { CheckFailure } from "../../types"; -export function truncateActual(value: unknown, maxLen = 200): unknown { - if (value === undefined || value === null) return value; - const str = String(value); - if (str.length <= maxLen) return value; - return str.slice(0, maxLen) + "..."; +export function errorFailure(phase: CheckFailure["phase"], path: string, message: string): CheckFailure { + return { + kind: "error", + message, + path, + phase, + }; } export function mismatchFailure( @@ -15,20 +17,18 @@ export function mismatchFailure( message: string, ): CheckFailure { return { - kind: "mismatch", - phase, - path, - expected, actual: truncateActual(actual), + expected, + kind: "mismatch", message, + path, + phase, }; } -export function errorFailure(phase: CheckFailure["phase"], path: string, message: string): CheckFailure { - return { - kind: "error", - phase, - path, - message, - }; +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) + "..."; } diff --git a/src/server/checker/runner/shared/operator.ts b/src/server/checker/runner/shared/operator.ts index f16c253..0bd4fb2 100644 --- a/src/server/checker/runner/shared/operator.ts +++ b/src/server/checker/runner/shared/operator.ts @@ -1,6 +1,59 @@ -import { isNil, isEmptyObject, isEqual, isPlainObject } from "es-toolkit"; +import { isEmptyObject, isEqual, isNil, isPlainObject } from "es-toolkit"; + import type { ExpectOperator, ExpectValue } from "../../types"; +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)) { + return applyOperator(actual, expected); + } + return applyOperator(actual, { equals: expected }); +} + export function evaluateJsonPath(json: unknown, path: string): unknown { if (!path.startsWith("$.")) return undefined; @@ -22,55 +75,3 @@ export function evaluateJsonPath(json: unknown, path: string): unknown { return current; } - -export function applyOperator(actual: unknown, op: ExpectOperator): boolean { - for (const [key, expected] of Object.entries(op)) { - if (expected === undefined) continue; - - switch (key) { - case "equals": - if (!isEqual(actual, expected)) return false; - break; - case "contains": - if (!String(actual).includes(expected as string)) return false; - break; - case "match": - if (!new RegExp(expected as string).test(String(actual))) 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 "exists": - if (expected) { - if (actual === undefined) return false; - } else { - if (actual !== undefined) return false; - } - break; - case "gte": - if (!(Number(actual) >= (expected as number))) return false; - break; - case "lte": - if (!(Number(actual) <= (expected as number))) return false; - break; - case "gt": - if (!(Number(actual) > (expected as number))) return false; - break; - case "lt": - if (!(Number(actual) < (expected as number))) return false; - break; - } - } - - return true; -} - -export function checkExpectValue(actual: unknown, expected: ExpectValue): boolean { - if (isPlainObject(expected)) { - return applyOperator(actual, expected as ExpectOperator); - } - return applyOperator(actual, { equals: expected as string | number | boolean | null }); -} diff --git a/src/server/checker/runner/shared/text.ts b/src/server/checker/runner/shared/text.ts index 9b186b9..6d7e218 100644 --- a/src/server/checker/runner/shared/text.ts +++ b/src/server/checker/runner/shared/text.ts @@ -1,18 +1,19 @@ import type { TextRule } from "../../types"; -import { applyOperator } from "./operator"; -import { mismatchFailure } from "./failure"; import type { ExpectResult } from "./duration"; +import { mismatchFailure } from "./failure"; +import { applyOperator } from "./operator"; + export function checkTextRules(text: string, rules: TextRule[], phase: string): ExpectResult { for (let i = 0; i < rules.length; i++) { const rule = rules[i]!; const path = `${phase}[${i}]`; if (!applyOperator(text, rule)) { return { - matched: false, failure: mismatchFailure(phase, path, rule, text, `${phase} rule at index ${i} mismatch`), + matched: false, }; } } - return { matched: true, failure: null }; + return { failure: null, matched: true }; } diff --git a/src/server/checker/runner/types.ts b/src/server/checker/runner/types.ts index 77d83ee..e1bc3ac 100644 --- a/src/server/checker/runner/types.ts +++ b/src/server/checker/runner/types.ts @@ -1,20 +1,19 @@ -import type { CheckResult } from "../types"; -import type { DefaultsConfig, ResolvedTarget, TargetConfig } from "../types"; +import type { CheckResult, DefaultsConfig, ResolvedTarget, TargetConfig } from "../types"; + +export interface Checker { + execute(target: ResolvedTarget, ctx: CheckerContext): Promise; + resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget; + serialize(target: ResolvedTarget): { config: string; target: string }; + readonly type: string; +} export interface CheckerContext { signal: AbortSignal; } export interface ResolveContext { - defaults: DefaultsConfig; configDir: string; defaultIntervalMs: number; + defaults: DefaultsConfig; defaultTimeoutMs: number; } - -export interface Checker { - readonly type: string; - resolve(target: TargetConfig, context: ResolveContext): ResolvedTarget; - execute(target: ResolvedTarget, ctx: CheckerContext): Promise; - serialize(target: ResolvedTarget): { target: string; config: string }; -} diff --git a/src/server/checker/size.ts b/src/server/checker/size.ts index 7b3a8ae..dd65d05 100644 --- a/src/server/checker/size.ts +++ b/src/server/checker/size.ts @@ -1,6 +1,6 @@ const SIZE_REGEX = /^(\d+(?:\.\d+)?)(B|KB|MB|GB)$/; -export function parseSize(value: string | number): number { +export function parseSize(value: number | string): number { if (typeof value === "number") return value; const match = SIZE_REGEX.exec(value); diff --git a/src/server/checker/store.ts b/src/server/checker/store.ts index dc33ede..13c130c 100644 --- a/src/server/checker/store.ts +++ b/src/server/checker/store.ts @@ -1,7 +1,9 @@ import { Database } from "bun:sqlite"; import { mkdirSync as fsMkdirSync } from "node:fs"; import { dirname } from "node:path"; + import type { CheckFailure, ResolvedTarget, StoredCheckResult, StoredTarget } from "./types"; + import { checkerRegistry } from "./runner"; const CREATE_TARGETS_TABLE = ` @@ -37,8 +39,8 @@ ON check_results (target_id, timestamp) `; export class ProbeStore { - private db: Database; private closed = false; + private db: Database; constructor(dbPath: string) { ensureDir(dirname(dbPath)); @@ -50,6 +52,211 @@ export class ProbeStore { this.db.run(CREATE_INDEX); } + close(): void { + this.closed = true; + this.db.close(); + } + + getAllTargetStats(): Map { + const rows = this.db + .query( + `SELECT target_id, COUNT(*) as totalChecks, + COALESCE(SUM(CASE WHEN matched = 1 THEN 1 ELSE 0 END), 0) as upCount + FROM check_results + GROUP BY target_id`, + ) + .all() as Array<{ target_id: number; totalChecks: number; upCount: number }>; + + const result = new Map(); + for (const row of rows) { + const availability = row.totalChecks > 0 ? Math.round((row.upCount / row.totalChecks) * 10000) / 100 : 0; + result.set(row.target_id, { availability, totalChecks: row.totalChecks }); + } + return result; + } + + getHistory( + targetId: number, + from: string, + to: string, + page = 1, + pageSize = 20, + ): { items: StoredCheckResult[]; page: number; pageSize: number; total: number } { + const countRow = this.db + .query("SELECT COUNT(*) as total FROM check_results WHERE target_id = ? AND timestamp >= ? AND timestamp <= ?") + .get(targetId, from, to) as { total: number }; + + const offset = (page - 1) * pageSize; + const items = this.db + .query( + "SELECT * FROM check_results WHERE target_id = ? AND timestamp >= ? AND timestamp <= ? ORDER BY timestamp DESC LIMIT ? OFFSET ?", + ) + .all(targetId, from, to, pageSize, offset) as StoredCheckResult[]; + + return { items, page, pageSize, total: countRow.total }; + } + + getLatestCheck(targetId: number): null | StoredCheckResult { + return this.db + .query("SELECT * FROM check_results WHERE target_id = ? ORDER BY timestamp DESC LIMIT 1") + .get(targetId) as null | StoredCheckResult; + } + + getLatestChecksMap(): Map { + const rows = this.db + .query( + `SELECT cr.* FROM check_results cr + INNER JOIN ( + SELECT target_id, MAX(timestamp) as max_ts + FROM check_results + GROUP BY target_id + ) latest ON cr.target_id = latest.target_id AND cr.timestamp = latest.max_ts`, + ) + .all() as StoredCheckResult[]; + return new Map(rows.map((r) => [r.target_id, r])); + } + + getRecentSamples( + targetId: number, + limit: number, + ): Array<{ duration_ms: null | number; matched: number; timestamp: string }> { + return this.db + .query( + "SELECT timestamp, duration_ms, matched FROM check_results WHERE target_id = ? ORDER BY timestamp DESC LIMIT ?", + ) + .all(targetId, limit) as Array<{ + duration_ms: null | number; + matched: number; + timestamp: string; + }>; + } + + getSummary(): { + down: number; + lastCheckTime: null | string; + total: number; + up: number; + } { + const targets = this.getTargets(); + const latestChecksMap = this.getLatestChecksMap(); + let up = 0; + let down = 0; + let lastCheckTime: null | string = null; + + for (const target of targets) { + const latest = latestChecksMap.get(target.id); + + if (latest) { + if (latest.matched) { + up++; + } else { + down++; + } + + if (!lastCheckTime || latest.timestamp > lastCheckTime) { + lastCheckTime = latest.timestamp; + } + } else { + down++; + } + } + + return { + down, + lastCheckTime, + total: targets.length, + up, + }; + } + + getTargetById(id: number): null | StoredTarget { + if (this.closed) return null; + return this.db.query("SELECT * FROM targets WHERE id = ?").get(id) as null | StoredTarget; + } + + getTargets(): StoredTarget[] { + if (this.closed) return []; + return this.db + .query("SELECT * FROM targets ORDER BY CASE WHEN grp='default' THEN 0 ELSE 1 END, id") + .all() as StoredTarget[]; + } + + getTargetStats(targetId: number): { + availability: number; + totalChecks: number; + } { + const row = this.db + .query( + `SELECT + COUNT(*) as totalChecks, + COALESCE(SUM(CASE WHEN matched = 1 THEN 1 ELSE 0 END), 0) as upCount + FROM check_results + WHERE target_id = ?`, + ) + .get(targetId) as { totalChecks: number; upCount: number }; + + const totalChecks = row.totalChecks; + const availability = totalChecks > 0 ? (row.upCount / totalChecks) * 100 : 0; + + return { + availability: Math.round(availability * 100) / 100, + totalChecks, + }; + } + + getTrend( + targetId: number, + from: string, + to: string, + ): Array<{ + availability: number; + avgDurationMs: null | number; + hour: string; + totalChecks: number; + }> { + return this.db + .query( + `SELECT + strftime('%Y-%m-%dT%H:00:00', timestamp) as hour, + AVG(CASE WHEN matched = 1 THEN duration_ms END) as avgDurationMs, + CASE WHEN COUNT(*) > 0 THEN (SUM(CASE WHEN matched = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*)) ELSE 0 END as availability, + COUNT(*) as totalChecks + FROM check_results + WHERE target_id = ? AND timestamp >= ? AND timestamp <= ? + GROUP BY hour + ORDER BY hour`, + ) + .all(targetId, from, to) as Array<{ + availability: number; + avgDurationMs: null | number; + hour: string; + totalChecks: number; + }>; + } + + insertCheckResult(result: { + durationMs: null | number; + failure: CheckFailure | null; + matched: boolean; + statusDetail: null | string; + targetId: number; + timestamp: string; + }): void { + if (this.closed) return; + this.db + .query( + "INSERT INTO check_results (target_id, timestamp, matched, duration_ms, status_detail, failure) VALUES (?, ?, ?, ?, ?, ?)", + ) + .run( + result.targetId, + result.timestamp, + result.matched ? 1 : 0, + result.durationMs, + result.statusDetail, + result.failure ? JSON.stringify(result.failure) : null, + ); + } + syncTargets(targets: ResolvedTarget[]): void { if (this.closed) return; const existingRows = this.db.query("SELECT id, name FROM targets").all() as Array<{ @@ -90,221 +297,16 @@ export class ProbeStore { tx(); } - - getTargets(): StoredTarget[] { - if (this.closed) return []; - return this.db - .query("SELECT * FROM targets ORDER BY CASE WHEN grp='default' THEN 0 ELSE 1 END, id") - .all() as StoredTarget[]; - } - - getTargetById(id: number): StoredTarget | null { - if (this.closed) return null; - return this.db.query("SELECT * FROM targets WHERE id = ?").get(id) as StoredTarget | null; - } - - insertCheckResult(result: { - targetId: number; - timestamp: string; - matched: boolean; - durationMs: number | null; - statusDetail: string | null; - failure: CheckFailure | null; - }): void { - if (this.closed) return; - this.db - .query( - "INSERT INTO check_results (target_id, timestamp, matched, duration_ms, status_detail, failure) VALUES (?, ?, ?, ?, ?, ?)", - ) - .run( - result.targetId, - result.timestamp, - result.matched ? 1 : 0, - result.durationMs, - result.statusDetail, - result.failure ? JSON.stringify(result.failure) : null, - ); - } - - getLatestCheck(targetId: number): StoredCheckResult | null { - return this.db - .query("SELECT * FROM check_results WHERE target_id = ? ORDER BY timestamp DESC LIMIT 1") - .get(targetId) as StoredCheckResult | null; - } - - getHistory( - targetId: number, - from: string, - to: string, - page = 1, - pageSize = 20, - ): { items: StoredCheckResult[]; total: number; page: number; pageSize: number } { - const countRow = this.db - .query("SELECT COUNT(*) as total FROM check_results WHERE target_id = ? AND timestamp >= ? AND timestamp <= ?") - .get(targetId, from, to) as { total: number }; - - const offset = (page - 1) * pageSize; - const items = this.db - .query( - "SELECT * FROM check_results WHERE target_id = ? AND timestamp >= ? AND timestamp <= ? ORDER BY timestamp DESC LIMIT ? OFFSET ?", - ) - .all(targetId, from, to, pageSize, offset) as StoredCheckResult[]; - - return { items, total: countRow.total, page, pageSize }; - } - - getTargetStats(targetId: number): { - totalChecks: number; - availability: number; - } { - const row = this.db - .query( - `SELECT - COUNT(*) as totalChecks, - COALESCE(SUM(CASE WHEN matched = 1 THEN 1 ELSE 0 END), 0) as upCount - FROM check_results - WHERE target_id = ?`, - ) - .get(targetId) as { totalChecks: number; upCount: number }; - - const totalChecks = row.totalChecks; - const availability = totalChecks > 0 ? (row.upCount / totalChecks) * 100 : 0; - - return { - totalChecks, - availability: Math.round(availability * 100) / 100, - }; - } - - getTrend( - targetId: number, - from: string, - to: string, - ): Array<{ - hour: string; - avgDurationMs: number | null; - availability: number; - totalChecks: number; - }> { - return this.db - .query( - `SELECT - strftime('%Y-%m-%dT%H:00:00', timestamp) as hour, - AVG(CASE WHEN matched = 1 THEN duration_ms END) as avgDurationMs, - CASE WHEN COUNT(*) > 0 THEN (SUM(CASE WHEN matched = 1 THEN 1 ELSE 0 END) * 100.0 / COUNT(*)) ELSE 0 END as availability, - COUNT(*) as totalChecks - FROM check_results - WHERE target_id = ? AND timestamp >= ? AND timestamp <= ? - GROUP BY hour - ORDER BY hour`, - ) - .all(targetId, from, to) as Array<{ - hour: string; - avgDurationMs: number | null; - availability: number; - totalChecks: number; - }>; - } - - getSummary(): { - total: number; - up: number; - down: number; - lastCheckTime: string | null; - } { - const targets = this.getTargets(); - const latestChecksMap = this.getLatestChecksMap(); - let up = 0; - let down = 0; - let lastCheckTime: string | null = null; - - for (const target of targets) { - const latest = latestChecksMap.get(target.id); - - if (latest) { - if (latest.matched) { - up++; - } else { - down++; - } - - if (!lastCheckTime || latest.timestamp > lastCheckTime) { - lastCheckTime = latest.timestamp; - } - } else { - down++; - } - } - - return { - total: targets.length, - up, - down, - lastCheckTime, - }; - } - - getRecentSamples( - targetId: number, - limit: number, - ): Array<{ timestamp: string; duration_ms: number | null; matched: number }> { - return this.db - .query( - "SELECT timestamp, duration_ms, matched FROM check_results WHERE target_id = ? ORDER BY timestamp DESC LIMIT ?", - ) - .all(targetId, limit) as Array<{ - timestamp: string; - duration_ms: number | null; - matched: number; - }>; - } - - getLatestChecksMap(): Map { - const rows = this.db - .query( - `SELECT cr.* FROM check_results cr - INNER JOIN ( - SELECT target_id, MAX(timestamp) as max_ts - FROM check_results - GROUP BY target_id - ) latest ON cr.target_id = latest.target_id AND cr.timestamp = latest.max_ts`, - ) - .all() as StoredCheckResult[]; - return new Map(rows.map((r) => [r.target_id, r])); - } - - getAllTargetStats(): Map { - const rows = this.db - .query( - `SELECT target_id, COUNT(*) as totalChecks, - COALESCE(SUM(CASE WHEN matched = 1 THEN 1 ELSE 0 END), 0) as upCount - FROM check_results - GROUP BY target_id`, - ) - .all() as Array<{ target_id: number; totalChecks: number; upCount: number }>; - - const result = new Map(); - for (const row of rows) { - const availability = row.totalChecks > 0 ? Math.round((row.upCount / row.totalChecks) * 10000) / 100 : 0; - result.set(row.target_id, { totalChecks: row.totalChecks, availability }); - } - return result; - } - - close(): void { - this.closed = true; - this.db.close(); - } -} - -function buildTargetDisplay(t: ResolvedTarget): string { - return checkerRegistry.get(t.type).serialize(t).target; } function buildTargetConfig(t: ResolvedTarget): string { return checkerRegistry.get(t.type).serialize(t).config; } +function buildTargetDisplay(t: ResolvedTarget): string { + return checkerRegistry.get(t.type).serialize(t).target; +} + function ensureDir(dir: string): void { try { fsMkdirSync(dir, { recursive: true }); diff --git a/src/server/checker/types.ts b/src/server/checker/types.ts index c6261a5..cc043d5 100644 --- a/src/server/checker/types.ts +++ b/src/server/checker/types.ts @@ -1,28 +1,14 @@ import type { CheckResult as ApiCheckResult, CheckFailure } from "../../shared/api"; -export type TargetType = "http" | "command"; +export type BodyRule = + | { contains: string } + | { css: CssRule } + | { json: JsonRule } + | { regex: string } + | { xpath: XpathRule }; -export interface ProbeConfig { - server?: ServerConfig; - runtime?: EngineRuntimeConfig; - defaults?: DefaultsConfig; - targets: TargetConfig[]; -} - -export interface ServerConfig { - host?: string; - port?: number; - dataDir?: string; -} - -export interface EngineRuntimeConfig { - maxConcurrentChecks?: number; -} - -export interface HttpDefaultsConfig { - method?: string; - headers?: Record; - maxBodyBytes?: string; +export interface CheckResult extends ApiCheckResult { + targetName: string; } export interface CommandDefaultsConfig { @@ -30,148 +16,162 @@ export interface CommandDefaultsConfig { maxOutputBytes?: string; } -export interface DefaultsConfig { - interval?: string; - timeout?: string; - http?: HttpDefaultsConfig; - command?: CommandDefaultsConfig; -} - -export interface HttpTargetConfig { - url: string; - method?: string; - headers?: Record; - body?: string; - maxBodyBytes?: string; -} - -export interface CommandTargetConfig { - exec: string; - args?: string[]; - cwd?: string; - env?: Record; - maxOutputBytes?: string; -} - -export type TargetConfig = BaseTargetConfig & - ({ type: "http"; http: HttpTargetConfig } | { type: "command"; command: CommandTargetConfig }); - -interface BaseTargetConfig { - name: string; - group?: string; - interval?: string; - timeout?: string; - expect?: ExpectConfig; -} - -export interface ExpectOperator { - equals?: string | number | boolean | null; - contains?: string; - match?: string; - empty?: boolean; - exists?: boolean; - gte?: number; - lte?: number; - gt?: number; - lt?: number; -} - -export type ExpectValue = string | number | boolean | null | ExpectOperator; - -export type TextRule = ExpectOperator; - -export type JsonRule = { path: string } & ExpectOperator; - -export type CssRule = { selector: string; attr?: string } & ExpectOperator; - -export type XpathRule = { path: string } & ExpectOperator; - -export type BodyRule = - | { contains: string } - | { regex: string } - | { json: JsonRule } - | { css: CssRule } - | { xpath: XpathRule }; - -export type HeaderExpect = string | ExpectOperator; - -export interface HttpExpectConfig { - status?: number[]; - maxDurationMs?: number; - headers?: Record; - body?: BodyRule[]; -} - export interface CommandExpectConfig { exitCode?: number[]; maxDurationMs?: number; - stdout?: TextRule[]; stderr?: TextRule[]; + stdout?: TextRule[]; } -export type ExpectConfig = HttpExpectConfig | CommandExpectConfig; - -export interface ResolvedHttpTarget { - type: "http"; - name: string; - group: string; - http: ResolvedHttpConfig; - intervalMs: number; - timeoutMs: number; - expect?: HttpExpectConfig; +export interface CommandTargetConfig { + args?: string[]; + cwd?: string; + env?: Record; + exec: string; + maxOutputBytes?: string; } -export interface ResolvedHttpConfig { - url: string; - method: string; - headers: Record; +export type CssRule = ExpectOperator & { attr?: string; selector: string }; + +export interface DefaultsConfig { + command?: CommandDefaultsConfig; + http?: HttpDefaultsConfig; + interval?: string; + timeout?: string; +} + +export interface EngineRuntimeConfig { + maxConcurrentChecks?: number; +} + +export type ExpectConfig = CommandExpectConfig | HttpExpectConfig; + +export interface ExpectOperator { + contains?: string; + empty?: boolean; + equals?: boolean | null | number | string; + exists?: boolean; + gt?: number; + gte?: number; + lt?: number; + lte?: number; + match?: string; +} + +export type ExpectValue = boolean | ExpectOperator | null | number | string; + +export type HeaderExpect = ExpectOperator | string; + +export interface HttpDefaultsConfig { + headers?: Record; + maxBodyBytes?: string; + method?: string; +} + +export interface HttpExpectConfig { + body?: BodyRule[]; + headers?: Record; + maxDurationMs?: number; + status?: number[]; +} + +export interface HttpTargetConfig { body?: string; - maxBodyBytes: number; + headers?: Record; + maxBodyBytes?: string; + method?: string; + url: string; } -export interface ResolvedCommandTarget { - type: "command"; - name: string; - group: string; - command: ResolvedCommandConfig; - intervalMs: number; - timeoutMs: number; - expect?: CommandExpectConfig; +export type JsonRule = ExpectOperator & { path: string }; + +export interface ProbeConfig { + defaults?: DefaultsConfig; + runtime?: EngineRuntimeConfig; + server?: ServerConfig; + targets: TargetConfig[]; } export interface ResolvedCommandConfig { - exec: string; args: string[]; cwd: string; env: Record; + exec: string; maxOutputBytes: number; } -export type ResolvedTarget = ResolvedHttpTarget | ResolvedCommandTarget; - -export type { CheckFailure }; -export interface CheckResult extends ApiCheckResult { - targetName: string; +export interface ResolvedCommandTarget { + command: ResolvedCommandConfig; + expect?: CommandExpectConfig; + group: string; + intervalMs: number; + name: string; + timeoutMs: number; + type: "command"; } -export interface StoredTarget { - id: number; +export interface ResolvedHttpConfig { + body?: string; + headers: Record; + maxBodyBytes: number; + method: string; + url: string; +} + +export interface ResolvedHttpTarget { + expect?: HttpExpectConfig; + group: string; + http: ResolvedHttpConfig; + intervalMs: number; name: string; - type: TargetType; - target: string; - config: string; - interval_ms: number; - timeout_ms: number; - expect: string | null; - grp: string; + timeoutMs: number; + type: "http"; +} + +export type ResolvedTarget = ResolvedCommandTarget | ResolvedHttpTarget; + +export interface ServerConfig { + dataDir?: string; + host?: string; + port?: number; } export interface StoredCheckResult { + duration_ms: null | number; + failure: null | string; id: number; + matched: number; + status_detail: null | string; target_id: number; timestamp: string; - matched: number; - duration_ms: number | null; - status_detail: string | null; - failure: string | null; +} + +export interface StoredTarget { + config: string; + expect: null | string; + grp: string; + id: number; + interval_ms: number; + name: string; + target: string; + timeout_ms: number; + type: TargetType; +} + +export type TargetConfig = BaseTargetConfig & + ({ command: CommandTargetConfig; type: "command" } | { http: HttpTargetConfig; type: "http" }); + +export type TargetType = "command" | "http"; + +export type { CheckFailure }; +export type TextRule = ExpectOperator; + +export type XpathRule = ExpectOperator & { path: string }; + +interface BaseTargetConfig { + expect?: ExpectConfig; + group?: string; + interval?: string; + name: string; + timeout?: string; } diff --git a/src/server/config.ts b/src/server/config.ts index dcd11b6..11d9ac4 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -1,3 +1,8 @@ +export interface RuntimeConfig { + host: string; + port: number; +} + export function readRuntimeConfig(argv: string[] = process.argv.slice(2)): { configPath: string } { if (argv.length === 0) { throw new Error("需要指定 YAML 配置文件路径\n用法: dial-server "); @@ -5,8 +10,3 @@ export function readRuntimeConfig(argv: string[] = process.argv.slice(2)): { con return { configPath: argv[0]! }; } - -export interface RuntimeConfig { - host: string; - port: number; -} diff --git a/src/server/dev.ts b/src/server/dev.ts index b57cb8e..318f99a 100644 --- a/src/server/dev.ts +++ b/src/server/dev.ts @@ -1,9 +1,9 @@ import { loadConfig } from "./checker/config-loader"; -import { ProbeStore } from "./checker/store"; import { ProbeEngine } from "./checker/engine"; -import { startServer } from "./server"; -import { readRuntimeConfig } from "./config"; import { registerCheckers } from "./checker/runner"; +import { ProbeStore } from "./checker/store"; +import { readRuntimeConfig } from "./config"; +import { startServer } from "./server"; async function main() { registerCheckers(); diff --git a/src/server/helpers.ts b/src/server/helpers.ts index 08d15a8..e87cf5d 100644 --- a/src/server/helpers.ts +++ b/src/server/helpers.ts @@ -1,6 +1,10 @@ import type { ApiErrorResponse, CheckFailure, CheckResult, HealthResponse, RuntimeMode } from "../shared/api"; import type { StoredCheckResult } from "./checker/types"; +export function allowsGetHead(method: string): boolean { + return method === "GET" || method === "HEAD"; +} + export function createApiError(error: string, status: number): ApiErrorResponse { return { error, status }; } @@ -16,9 +20,23 @@ export function createHeaders(mode: RuntimeMode, init: HeadersInit): Headers { return headers; } +export function createHealthResponse(): HealthResponse { + return { + ok: true, + service: "dial-server", + timestamp: new Date().toISOString(), + }; +} + +export function formatDuration(ms: number): string { + if (ms >= 60000 && ms % 60000 === 0) return `${ms / 60000}m`; + if (ms >= 1000 && ms % 1000 === 0) return `${ms / 1000}s`; + return `${ms}ms`; +} + export function jsonResponse( body: unknown, - options: { method?: string; mode: RuntimeMode; status?: number; headers?: HeadersInit }, + options: { headers?: HeadersInit; method?: string; mode: RuntimeMode; status?: number }, ): Response { const headers = createHeaders(options.mode, { "Content-Type": "application/json; charset=utf-8", @@ -27,29 +45,11 @@ export function jsonResponse( const responseBody = options.method === "HEAD" ? null : JSON.stringify(body); return new Response(responseBody, { - status: options.status, headers, + status: options.status, }); } -export function methodNotAllowedResponse(allow: string[], mode: RuntimeMode): Response { - return jsonResponse(createApiError("Method not allowed", 405), { - mode, - status: 405, - headers: { Allow: allow.join(", ") }, - }); -} - -export function allowsGetHead(method: string): boolean { - return method === "GET" || method === "HEAD"; -} - -export function formatDuration(ms: number): string { - if (ms >= 60000 && ms % 60000 === 0) return `${ms / 60000}m`; - if (ms >= 1000 && ms % 1000 === 0) return `${ms / 1000}s`; - return `${ms}ms`; -} - export function mapCheckResult(row: StoredCheckResult): CheckResult { let failure: CheckFailure | null = null; if (row.failure) { @@ -62,18 +62,18 @@ export function mapCheckResult(row: StoredCheckResult): CheckResult { } return { - timestamp: row.timestamp, - matched: row.matched === 1, durationMs: row.duration_ms, - statusDetail: row.status_detail, failure, + matched: row.matched === 1, + statusDetail: row.status_detail, + timestamp: row.timestamp, }; } -export function createHealthResponse(): HealthResponse { - return { - ok: true, - service: "dial-server", - timestamp: new Date().toISOString(), - }; +export function methodNotAllowedResponse(allow: string[], mode: RuntimeMode): Response { + return jsonResponse(createApiError("Method not allowed", 405), { + headers: { Allow: allow.join(", ") }, + mode, + status: 405, + }); } diff --git a/src/server/middleware.ts b/src/server/middleware.ts index f9bff5e..05ed9ae 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -1,42 +1,19 @@ import type { RuntimeMode } from "../shared/api"; + import { allowsGetHead, createApiError, jsonResponse, methodNotAllowedResponse } from "./helpers"; -export function guardGetHead(method: string, mode: RuntimeMode): Response | null { +export function guardGetHead(method: string, mode: RuntimeMode): null | Response { if (!allowsGetHead(method)) { return methodNotAllowedResponse(["GET", "HEAD"], mode); } return null; } -export function validateTargetId(idStr: string, mode: RuntimeMode): { id: number } | Response { - const id = Number(idStr); - if (!Number.isInteger(id) || id <= 0) { - return jsonResponse(createApiError("Invalid target ID", 400), { mode, status: 400 }); - } - return { id }; -} - -export function validateTimeRange( - from: string | null, - to: string | null, - mode: RuntimeMode, -): { from: string; to: string } | Response { - if (!from || !to) { - return jsonResponse(createApiError("from and to parameters are required", 400), { mode, status: 400 }); - } - - if (isNaN(new Date(from).getTime()) || isNaN(new Date(to).getTime())) { - return jsonResponse(createApiError("Invalid from or to parameter format", 400), { mode, status: 400 }); - } - - return { from, to }; -} - export function validatePagination( - pageParam: string | null, - pageSizeParam: string | null, + pageParam: null | string, + pageSizeParam: null | string, mode: RuntimeMode, -): { page: number; pageSize: number } | Response { +): Response | { page: number; pageSize: number } { let page = 1; let pageSize = 20; @@ -56,3 +33,27 @@ export function validatePagination( return { page, pageSize }; } + +export function validateTargetId(idStr: string, mode: RuntimeMode): Response | { id: number } { + const id = Number(idStr); + if (!Number.isInteger(id) || id <= 0) { + return jsonResponse(createApiError("Invalid target ID", 400), { mode, status: 400 }); + } + return { id }; +} + +export function validateTimeRange( + from: null | string, + to: null | string, + mode: RuntimeMode, +): Response | { from: string; to: string } { + if (!from || !to) { + return jsonResponse(createApiError("from and to parameters are required", 400), { mode, status: 400 }); + } + + if (isNaN(new Date(from).getTime()) || isNaN(new Date(to).getTime())) { + return jsonResponse(createApiError("Invalid from or to parameter format", 400), { mode, status: 400 }); + } + + return { from, to }; +} diff --git a/src/server/routes/health.ts b/src/server/routes/health.ts index dd05b2e..d698d89 100644 --- a/src/server/routes/health.ts +++ b/src/server/routes/health.ts @@ -1,5 +1,6 @@ import type { RuntimeMode } from "../../shared/api"; -import { createHealthResponse, jsonResponse, allowsGetHead, methodNotAllowedResponse } from "../helpers"; + +import { allowsGetHead, createHealthResponse, jsonResponse, methodNotAllowedResponse } from "../helpers"; export function handleHealth(method: string, mode: RuntimeMode): Response { if (!allowsGetHead(method)) { diff --git a/src/server/routes/history.ts b/src/server/routes/history.ts index cda65c8..448d31f 100644 --- a/src/server/routes/history.ts +++ b/src/server/routes/history.ts @@ -1,7 +1,8 @@ -import type { RuntimeMode, HistoryResponse } from "../../shared/api"; +import type { HistoryResponse, RuntimeMode } from "../../shared/api"; import type { ProbeStore } from "../checker/store"; + import { jsonResponse, mapCheckResult } from "../helpers"; -import { validateTargetId, validateTimeRange, validatePagination } from "../middleware"; +import { validatePagination, validateTargetId, validateTimeRange } from "../middleware"; export function handleHistory(idStr: string, url: URL, method: string, store: ProbeStore, mode: RuntimeMode): Response { const idResult = validateTargetId(idStr, mode); @@ -21,9 +22,9 @@ export function handleHistory(idStr: string, url: URL, method: string, store: Pr const result = store.getHistory(idResult.id, timeResult.from, timeResult.to, pageResult.page, pageResult.pageSize); const response: HistoryResponse = { items: result.items.map(mapCheckResult), - total: result.total, page: result.page, pageSize: result.pageSize, + total: result.total, }; return jsonResponse(response, { method, mode }); diff --git a/src/server/routes/summary.ts b/src/server/routes/summary.ts index 59226d6..7aa19a7 100644 --- a/src/server/routes/summary.ts +++ b/src/server/routes/summary.ts @@ -1,14 +1,15 @@ import type { RuntimeMode, SummaryResponse } from "../../shared/api"; import type { ProbeStore } from "../checker/store"; + import { jsonResponse } from "../helpers"; export function handleSummary(store: ProbeStore, method: string, mode: RuntimeMode): Response { const summary = store.getSummary(); const response: SummaryResponse = { - total: summary.total, - up: summary.up, down: summary.down, lastCheckTime: summary.lastCheckTime, + total: summary.total, + up: summary.up, }; return jsonResponse(response, { method, mode }); diff --git a/src/server/routes/targets.ts b/src/server/routes/targets.ts index 5374497..f0134b5 100644 --- a/src/server/routes/targets.ts +++ b/src/server/routes/targets.ts @@ -1,5 +1,6 @@ import type { RuntimeMode, TargetStatus } from "../../shared/api"; import type { ProbeStore } from "../checker/store"; + import { formatDuration, jsonResponse, mapCheckResult } from "../helpers"; export function handleTargets(store: ProbeStore, method: string, mode: RuntimeMode): Response { @@ -9,26 +10,26 @@ export function handleTargets(store: ProbeStore, method: string, mode: RuntimeMo const result: TargetStatus[] = targets.map((target) => { const latest = latestChecksMap.get(target.id) ?? null; - const stats = allStats.get(target.id) ?? { totalChecks: 0, availability: 0 }; + const stats = allStats.get(target.id) ?? { availability: 0, totalChecks: 0 }; const recentSamples = store.getRecentSamples(target.id, 30); return { - id: target.id, - name: target.name, - type: target.type, - target: target.target, group: target.grp, + id: target.id, interval: formatDuration(target.interval_ms), latestCheck: latest ? mapCheckResult(latest) : null, + name: target.name, recentSamples: recentSamples.map((s) => ({ - timestamp: s.timestamp, durationMs: s.duration_ms, + timestamp: s.timestamp, up: s.matched === 1, })), stats: { - totalChecks: stats.totalChecks, availability: stats.availability, + totalChecks: stats.totalChecks, }, + target: target.target, + type: target.type, }; }); diff --git a/src/server/routes/trend.ts b/src/server/routes/trend.ts index 98a7105..9dd8b84 100644 --- a/src/server/routes/trend.ts +++ b/src/server/routes/trend.ts @@ -1,5 +1,6 @@ import type { RuntimeMode, TrendPoint } from "../../shared/api"; import type { ProbeStore } from "../checker/store"; + import { jsonResponse } from "../helpers"; import { validateTargetId, validateTimeRange } from "../middleware"; @@ -16,9 +17,9 @@ export function handleTrend(idStr: string, url: URL, method: string, store: Prob if (timeResult instanceof Response) return timeResult; const trend: TrendPoint[] = store.getTrend(idResult.id, timeResult.from, timeResult.to).map((row) => ({ - hour: row.hour, - avgDurationMs: row.avgDurationMs, availability: Math.round(row.availability * 100) / 100, + avgDurationMs: row.avgDurationMs, + hour: row.hour, totalChecks: row.totalChecks, })); diff --git a/src/server/server.ts b/src/server/server.ts index e4f5192..bc8b933 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,9 +1,10 @@ import type { RuntimeMode } from "../shared/api"; import type { StaticAssets } from "./app"; import type { ProbeStore } from "./checker/store"; -import { createFetchHandler } from "./app"; import type { RuntimeConfig } from "./config"; +import { createFetchHandler } from "./app"; + export interface StartServerOptions { config: RuntimeConfig; mode: RuntimeMode; @@ -14,13 +15,13 @@ export interface StartServerOptions { export function startServer(options: StartServerOptions) { const { config, mode, staticAssets, store } = options; const server = Bun.serve({ - hostname: config.host, - port: config.port, fetch: createFetchHandler({ mode, staticAssets, store, }), + hostname: config.host, + port: config.port, }); console.log(`DiAL listening on ${server.url}`); diff --git a/src/server/static.ts b/src/server/static.ts index de0738d..ed029eb 100644 --- a/src/server/static.ts +++ b/src/server/static.ts @@ -1,45 +1,7 @@ import type { RuntimeMode } from "../shared/api"; -import { createHeaders } from "./helpers"; import type { StaticAssets } from "./app"; -export function serveStaticAsset(pathname: string, staticAssets: StaticAssets, mode: RuntimeMode): Response { - if (pathname === "/") { - return htmlResponse(staticAssets.indexHtml, mode); - } - - const asset = staticAssets.files[pathname]; - - if (asset) { - return new Response(asset, { - headers: createHeaders(mode, { - "Content-Type": contentTypeFor(pathname), - "Cache-Control": "public, max-age=31536000, immutable", - }), - }); - } - - if (pathname.startsWith("/assets/") || hasFileExtension(pathname)) { - return new Response("Not Found", { - status: 404, - headers: createHeaders(mode, { "Content-Type": "text/plain; charset=utf-8" }), - }); - } - - return htmlResponse(staticAssets.indexHtml, mode); -} - -export function htmlResponse(indexHtml: Blob, mode: RuntimeMode): Response { - return new Response(indexHtml, { - headers: createHeaders(mode, { - "Content-Type": "text/html; charset=utf-8", - "Cache-Control": "no-cache", - }), - }); -} - -export function hasFileExtension(pathname: string): boolean { - return /\/[^/]+\.[^/]+$/.test(pathname); -} +import { createHeaders } from "./helpers"; export function contentTypeFor(pathname: string): string { if (pathname.endsWith(".js") || pathname.endsWith(".mjs")) return "text/javascript; charset=utf-8"; @@ -52,3 +14,42 @@ export function contentTypeFor(pathname: string): string { return "application/octet-stream"; } + +export function hasFileExtension(pathname: string): boolean { + return /\/[^/]+\.[^/]+$/.test(pathname); +} + +export function htmlResponse(indexHtml: Blob, mode: RuntimeMode): Response { + return new Response(indexHtml, { + headers: createHeaders(mode, { + "Cache-Control": "no-cache", + "Content-Type": "text/html; charset=utf-8", + }), + }); +} + +export function serveStaticAsset(pathname: string, staticAssets: StaticAssets, mode: RuntimeMode): Response { + if (pathname === "/") { + return htmlResponse(staticAssets.indexHtml, mode); + } + + const asset = staticAssets.files[pathname]; + + if (asset) { + return new Response(asset, { + headers: createHeaders(mode, { + "Cache-Control": "public, max-age=31536000, immutable", + "Content-Type": contentTypeFor(pathname), + }), + }); + } + + if (pathname.startsWith("/assets/") || hasFileExtension(pathname)) { + return new Response("Not Found", { + headers: createHeaders(mode, { "Content-Type": "text/plain; charset=utf-8" }), + status: 404, + }); + } + + return htmlResponse(staticAssets.indexHtml, mode); +} diff --git a/src/shared/api.ts b/src/shared/api.ts index 09864a2..cd8713b 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -1,4 +1,24 @@ -export type RuntimeMode = "development" | "production" | "test"; +export interface ApiErrorResponse { + error: string; + status: number; +} + +export interface CheckFailure { + actual?: unknown; + expected?: unknown; + kind: "error" | "mismatch"; + message: string; + path: string; + phase: string; +} + +export interface CheckResult { + durationMs: null | number; + failure: CheckFailure | null; + matched: boolean; + statusDetail: null | string; + timestamp: string; +} export interface HealthResponse { ok: true; @@ -6,68 +26,48 @@ export interface HealthResponse { timestamp: string; } -export interface ApiErrorResponse { - error: string; - status: number; -} - -export interface SummaryResponse { +export interface HistoryResponse { + items: CheckResult[]; + page: number; + pageSize: number; total: number; - up: number; - down: number; - lastCheckTime: string | null; } export interface RecentSample { + durationMs: null | number; timestamp: string; - durationMs: number | null; up: boolean; } -export interface TargetStatus { - id: number; - name: string; - type: string; - target: string; - group: string; - interval: string; - latestCheck: CheckResult | null; - stats: TargetStats; - recentSamples: RecentSample[]; +export type RuntimeMode = "development" | "production" | "test"; + +export interface SummaryResponse { + down: number; + lastCheckTime: null | string; + total: number; + up: number; } export interface TargetStats { - totalChecks: number; availability: number; + totalChecks: number; } -export interface HistoryResponse { - items: CheckResult[]; - total: number; - page: number; - pageSize: number; -} - -export interface CheckResult { - timestamp: string; - matched: boolean; - durationMs: number | null; - statusDetail: string | null; - failure: CheckFailure | null; -} - -export interface CheckFailure { - kind: "error" | "mismatch"; - phase: string; - path: string; - expected?: unknown; - actual?: unknown; - message: string; +export interface TargetStatus { + group: string; + id: number; + interval: string; + latestCheck: CheckResult | null; + name: string; + recentSamples: RecentSample[]; + stats: TargetStats; + target: string; + type: string; } export interface TrendPoint { - hour: string; - avgDurationMs: number | null; availability: number; + avgDurationMs: null | number; + hour: string; totalChecks: number; } diff --git a/src/web/app.tsx b/src/web/app.tsx index bd2b212..3bcfdb5 100644 --- a/src/web/app.tsx +++ b/src/web/app.tsx @@ -1,27 +1,28 @@ import { Alert, Loading, Typography } from "tdesign-react"; -import { useSummary, useTargets, useTargetDetail } from "./hooks/useTargetDetail"; + import { SummaryCards } from "./components/SummaryCards"; import { TargetBoard } from "./components/TargetBoard"; import { TargetDetailDrawer } from "./components/TargetDetailDrawer"; +import { useSummary, useTargetDetail, useTargets } from "./hooks/useTargetDetail"; export function App() { - const { data: summary, isLoading: summaryLoading, error: summaryError } = useSummary(); - const { data: targets, isLoading: targetsLoading, error: targetsError } = useTargets(); + const { data: summary, error: summaryError, isLoading: summaryLoading } = useSummary(); + const { data: targets, error: targetsError, isLoading: targetsLoading } = useTargets(); const { - selectedTarget, - trendData, - trendLoading, + closeDrawer, + handlePageChange, + handleTimeChange, historyData, historyLoading, + openDrawer, + selectedTarget, timeFrom, timeTo, - openDrawer, - closeDrawer, - handleTimeChange, - handlePageChange, + trendData, + trendLoading, } = useTargetDetail(); - const error = summaryError || targetsError; + const error = summaryError ?? targetsError; return (
@@ -30,29 +31,29 @@ export function App() { 统一拨测平台 - {error && } + {error && } {summaryLoading && targetsLoading ? ( ) : ( <> - + )}
); diff --git a/src/web/components/GroupHeader.tsx b/src/web/components/GroupHeader.tsx index 6096638..0ce9cb3 100644 --- a/src/web/components/GroupHeader.tsx +++ b/src/web/components/GroupHeader.tsx @@ -1,25 +1,25 @@ import { Tag, Typography } from "tdesign-react"; interface GroupHeaderProps { + down: number; name: string; total: number; up: number; - down: number; } -export function GroupHeader({ name, total, up, down }: GroupHeaderProps) { +export function GroupHeader({ down, name, total, up }: GroupHeaderProps) { const displayName = name === "default" ? "默认分组" : name; return (
{displayName} - + {total} - + {up} - + {down}
diff --git a/src/web/components/StatusBar.tsx b/src/web/components/StatusBar.tsx index 7d705bc..4799744 100644 --- a/src/web/components/StatusBar.tsx +++ b/src/web/components/StatusBar.tsx @@ -9,12 +9,12 @@ export function StatusBar({ samples }: StatusBarProps) { if (sample) { blocks.push( , ); } else { - blocks.push(); + blocks.push(); } } diff --git a/src/web/components/StatusDonut.tsx b/src/web/components/StatusDonut.tsx index 8534780..0e852dd 100644 --- a/src/web/components/StatusDonut.tsx +++ b/src/web/components/StatusDonut.tsx @@ -1,15 +1,15 @@ -import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; +import { Cell, Pie, PieChart, ResponsiveContainer } from "recharts"; interface StatusDonutProps { - up: number; down: number; + up: number; } const UP_COLOR = "var(--td-success-color)"; const DOWN_COLOR = "var(--td-error-color)"; const EMPTY_COLOR = "var(--td-bg-color-component-disabled)"; -export function StatusDonut({ up, down }: StatusDonutProps) { +export function StatusDonut({ down, up }: StatusDonutProps) { const total = up + down; const availability = total > 0 ? ((up / total) * 100).toFixed(1) : "-"; @@ -25,11 +25,11 @@ export function StatusDonut({ up, down }: StatusDonutProps) { return (
- + - + {data.map((_, index) => ( - + ))} diff --git a/src/web/components/SummaryCards.tsx b/src/web/components/SummaryCards.tsx index e413cd8..f29dc06 100644 --- a/src/web/components/SummaryCards.tsx +++ b/src/web/components/SummaryCards.tsx @@ -1,25 +1,26 @@ -import { Row, Col, Card, Statistic } from "tdesign-react"; +import { Card, Col, Row, Statistic } from "tdesign-react"; + import type { SummaryResponse } from "../../shared/api"; interface SummaryCardsProps { - summary: SummaryResponse | null; + summary: null | SummaryResponse; } export function SummaryCards({ summary }: SummaryCardsProps) { if (!summary) return null; const cards = [ - { label: "全部目标", value: summary.total, color: "blue" as const }, - { label: "正常", value: summary.up, color: "green" as const }, - { label: "异常", value: summary.down, color: "red" as const }, + { color: "blue" as const, label: "全部目标", value: summary.total }, + { color: "green" as const, label: "正常", value: summary.up }, + { color: "red" as const, label: "异常", value: summary.down }, ]; return ( - + {cards.map((card) => ( - + ))} diff --git a/src/web/components/TargetBoard.tsx b/src/web/components/TargetBoard.tsx index eb85c0c..87a428a 100644 --- a/src/web/components/TargetBoard.tsx +++ b/src/web/components/TargetBoard.tsx @@ -1,13 +1,15 @@ import { Space } from "tdesign-react"; + import type { TargetStatus } from "../../shared/api"; + import { TargetGroup } from "./TargetGroup"; interface TargetBoardProps { - targets: TargetStatus[]; onTargetClick: (target: TargetStatus) => void; + targets: TargetStatus[]; } -export function TargetBoard({ targets, onTargetClick }: TargetBoardProps) { +export function TargetBoard({ onTargetClick, targets }: TargetBoardProps) { const groups = new Map(); for (const target of targets) { const group = target.group; @@ -25,9 +27,9 @@ export function TargetBoard({ targets, onTargetClick }: TargetBoardProps) { }); return ( - + {sortedGroups.map(([name, groupTargets]) => ( - + ))} ); diff --git a/src/web/components/TargetDetailDrawer.tsx b/src/web/components/TargetDetailDrawer.tsx index dc8c485..bda9bbe 100644 --- a/src/web/components/TargetDetailDrawer.tsx +++ b/src/web/components/TargetDetailDrawer.tsx @@ -1,96 +1,99 @@ -import { useState, useCallback } from "react"; +import type { TabValue } from "tdesign-react"; + +import { useCallback, useState } from "react"; import { - Drawer, - Tabs, - RadioGroup, - DateRangePicker, - Tag, - Row, Col, - Statistic, + DateRangePicker, Descriptions, - Skeleton, - PrimaryTable, Divider, + Drawer, + PrimaryTable, + RadioGroup, + Row, + Skeleton, Space, + Statistic, + Tabs, + Tag, Typography, } from "tdesign-react"; -import type { TabValue } from "tdesign-react"; -import type { CheckResult, TargetStatus, TrendPoint, HistoryResponse } from "../../shared/api"; -import { StatusDot } from "./StatusDot"; -import { StatusDonut } from "./StatusDonut"; -import { TrendChart } from "./TrendChart"; + +import type { CheckResult, HistoryResponse, TargetStatus, TrendPoint } from "../../shared/api"; + import { getTargetTypeDisplay } from "../constants/target-type-display"; import { subtractHours } from "../utils/time"; +import { StatusDonut } from "./StatusDonut"; +import { StatusDot } from "./StatusDot"; +import { TrendChart } from "./TrendChart"; interface TargetDetailDrawerProps { - target: TargetStatus | null; - trendData: TrendPoint[]; - trendLoading: boolean; historyData: HistoryResponse; historyLoading: boolean; + onClose: () => void; + onPageChange: (page: number) => void; + onTimeChange: (from: string, to: string) => void; + target: null | TargetStatus; timeFrom: string; timeTo: string; - onTimeChange: (from: string, to: string) => void; - onPageChange: (page: number) => void; - onClose: () => void; + trendData: TrendPoint[]; + trendLoading: boolean; } const TIME_SHORTCUTS = [ - { label: "1小时", hours: 1, value: "1h" }, - { label: "6小时", hours: 6, value: "6h" }, - { label: "24小时", hours: 24, value: "24h" }, - { label: "7天", hours: 168, value: "7d" }, + { hours: 1, label: "1小时", value: "1h" }, + { hours: 6, label: "6小时", value: "6h" }, + { hours: 24, label: "24小时", value: "24h" }, + { hours: 168, label: "7天", value: "7d" }, ] as const; const HISTORY_COLUMNS = [ { + cell: ({ row }: { col: unknown; colIndex: number; row: CheckResult; rowIndex: number }) => ( + + ), colKey: "matched", title: "#", width: 40, - cell: ({ row }: { row: CheckResult; rowIndex: number; col: unknown; colIndex: number }) => ( - - ), }, { - colKey: "timestamp", - title: "时间", - width: 180, - cell: ({ row }: { row: CheckResult; rowIndex: number; col: unknown; colIndex: number }) => { + cell: ({ row }: { col: unknown; colIndex: number; row: CheckResult; rowIndex: number }) => { const d = new Date(row.timestamp); const pad = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }, + colKey: "timestamp", + title: "时间", + width: 180, }, { + align: "center" as const, + cell: ({ row }: { col: unknown; colIndex: number; row: CheckResult; rowIndex: number }) => + row.durationMs !== null ? Math.round(row.durationMs) : "-", colKey: "durationMs", title: "耗时(ms)", width: 96, - align: "center" as const, - cell: ({ row }: { row: CheckResult; rowIndex: number; col: unknown; colIndex: number }) => - row.durationMs !== null ? Math.round(row.durationMs) : "-", }, { - colKey: "statusDetail", - title: "详情", - cell: ({ row }: { row: CheckResult; rowIndex: number; col: unknown; colIndex: number }) => { + cell: ({ row }: { col: unknown; colIndex: number; row: CheckResult; rowIndex: number }) => { const parts = [row.statusDetail, row.failure?.message].filter(Boolean); return parts.length > 0 ? parts.join(":") : "-"; }, + colKey: "statusDetail", + title: "详情", }, ]; export function TargetDetailDrawer({ - target, - trendData, - trendLoading, historyData, historyLoading, + onClose, + onPageChange, + onTimeChange, + target, timeFrom, timeTo, - onTimeChange, - onPageChange, - onClose, + trendData, + trendLoading, }: TargetDetailDrawerProps) { const [activeShortcut, setActiveShortcut] = useState("24h"); const [activeTab, setActiveTab] = useState("overview"); @@ -108,8 +111,8 @@ export function TargetDetailDrawer({ ); const handleDateRangeChange = useCallback( - (value: Array) => { - if (value && value.length === 2) { + (value: Array) => { + if (value?.length === 2) { onTimeChange(new Date(value[0]!).toISOString(), new Date(value[1]!).toISOString()); setActiveShortcut(""); } @@ -126,11 +129,7 @@ export function TargetDetailDrawer({ return ( @@ -140,44 +139,48 @@ export function TargetDetailDrawer({ } + onClose={onClose} + placement="right" + size="60%" + visible={!!target} > - + ({ label: s.label, value: s.value }))} onChange={handleShortcut} + options={TIME_SHORTCUTS.map((s) => ({ label: s.label, value: s.value }))} + theme="button" + value={activeShortcut} + variant="default-filled" /> - setActiveTab(val)}> - - + setActiveTab(val)} value={activeTab}> + + 统计 - + - + - + - + @@ -185,38 +188,38 @@ export function TargetDetailDrawer({ {trendLoading ? : } 状态分布 - + 基本信息 - + { + if (current) onPageChange(current); + }} pagination={{ current: historyData.page, pageSize: historyData.pageSize, total: historyData.total, }} - onPageChange={({ current }) => { - if (current) onPageChange(current); - }} + rowKey="timestamp" /> diff --git a/src/web/components/TargetGroup.tsx b/src/web/components/TargetGroup.tsx index 02a8435..a7ecd39 100644 --- a/src/web/components/TargetGroup.tsx +++ b/src/web/components/TargetGroup.tsx @@ -1,36 +1,38 @@ -import type { TargetStatus } from "../../shared/api"; -import { GroupHeader } from "./GroupHeader"; import { PrimaryTable } from "tdesign-react"; + +import type { TargetStatus } from "../../shared/api"; + import { TARGET_TABLE_COLUMNS } from "../constants/target-table-columns"; +import { GroupHeader } from "./GroupHeader"; interface TargetGroupProps { name: string; - targets: TargetStatus[]; onTargetClick: (target: TargetStatus) => void; + targets: TargetStatus[]; } -export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps) { +export function TargetGroup({ name, onTargetClick, targets }: TargetGroupProps) { const up = targets.filter((t) => t.latestCheck?.matched).length; const down = targets.length - up; return (
- + onTargetClick(row)} + rowClassName={({ row }) => { + const target = row; + return target.latestCheck?.matched === false ? "row-down" : ""; + }} rowKey="id" size="small" stripe - hover - bordered - defaultSort={[{ sortBy: "latestCheck.matched", descending: true }]} - onRowClick={({ row }) => onTargetClick(row as TargetStatus)} - rowClassName={({ row }) => { - const target = row as TargetStatus; - return target.latestCheck?.matched === false ? "row-down" : ""; - }} - className="clickable-table" />
); diff --git a/src/web/components/TrendChart.tsx b/src/web/components/TrendChart.tsx index a526114..e52428b 100644 --- a/src/web/components/TrendChart.tsx +++ b/src/web/components/TrendChart.tsx @@ -1,4 +1,5 @@ -import { Line, LineChart, ResponsiveContainer, XAxis, YAxis, Tooltip, CartesianGrid } from "recharts"; +import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; + import type { TrendPoint } from "../../shared/api"; interface TrendChartProps { @@ -22,23 +23,23 @@ export function TrendChart({ data, loading }: TrendChartProps) { return (
- + - - + + { @@ -50,22 +51,22 @@ export function TrendChart({ data, loading }: TrendChartProps) { }} /> diff --git a/src/web/constants/target-table-columns.tsx b/src/web/constants/target-table-columns.tsx index deed4b1..05114b9 100644 --- a/src/web/constants/target-table-columns.tsx +++ b/src/web/constants/target-table-columns.tsx @@ -1,89 +1,92 @@ -import type { PrimaryTableCol, PrimaryTableCellParams } from "tdesign-react"; -import { Tag, Progress } from "tdesign-react"; -import type { TargetStatus } from "../../shared/api"; -import { StatusDot } from "../components/StatusDot"; -import { StatusBar } from "../components/StatusBar"; -import { getTargetTypeDisplay } from "./target-type-display"; -import { getAvailabilityProgressColor } from "./color-threshold"; -import { availabilitySorter, latencySorter, nameSorter } from "./target-table-sorters"; -import { statusFilter, typeFilter } from "./target-table-filters"; +import type { PrimaryTableCellParams, PrimaryTableCol } from "tdesign-react"; -export const TARGET_TABLE_COLUMNS: PrimaryTableCol[] = [ +import { Progress, Tag } from "tdesign-react"; + +import type { TargetStatus } from "../../shared/api"; + +import { StatusBar } from "../components/StatusBar"; +import { StatusDot } from "../components/StatusDot"; +import { getAvailabilityProgressColor } from "./color-threshold"; +import { statusFilter, typeFilter } from "./target-table-filters"; +import { availabilitySorter, latencySorter, nameSorter } from "./target-table-sorters"; +import { getTargetTypeDisplay } from "./target-type-display"; + +export const TARGET_TABLE_COLUMNS: Array> = [ { + align: "center", + cell: ({ row }: PrimaryTableCellParams) => , colKey: "latestCheck.matched", + filter: statusFilter, + fixed: "left", title: "#", width: 60, - fixed: "left", - align: "center", - filter: statusFilter, - cell: ({ row }: PrimaryTableCellParams) => , }, { colKey: "name", - title: "名称", ellipsis: true, sorter: nameSorter, sortType: "all", + title: "名称", }, { - colKey: "type", - title: "类型", - width: 80, - filter: typeFilter, cell: ({ row }: PrimaryTableCellParams) => ( {getTargetTypeDisplay(row.type)} ), + colKey: "type", + filter: typeFilter, + title: "类型", + width: 80, }, { - colKey: "stats.availability", - title: "可用率", - width: 160, - sorter: availabilitySorter, - sortType: "all", cell: ({ row }: PrimaryTableCellParams) => { const availability = row.stats?.availability; if (availability === undefined || availability === null) return "-"; const color = getAvailabilityProgressColor(availability); return ( ); }, + colKey: "stats.availability", + sorter: availabilitySorter, + sortType: "all", + title: "可用率", + width: 160, }, { + cell: ({ row }: PrimaryTableCellParams) => , colKey: "recentSamples", title: "最近状态", width: 220, - cell: ({ row }: PrimaryTableCellParams) => , }, { - colKey: "latestCheck.durationMs", - title: "延迟", - width: 80, align: "right", - sorter: latencySorter, - sortType: "all", cell: ({ row }: PrimaryTableCellParams) => { const ms = row.latestCheck?.durationMs; if (ms === null || ms === undefined) return -; const colorClass = ms <= 100 ? "latency-ok" : ms <= 500 ? "latency-warn" : "latency-error"; return {Math.round(ms)}ms; }, + colKey: "latestCheck.durationMs", + sorter: latencySorter, + sortType: "all", + title: "延迟", + width: 80, }, { + align: "center", colKey: "interval", title: "间隔", width: 72, - align: "center", }, ]; -export { statusSorter, availabilitySorter, latencySorter, nameSorter } from "./target-table-sorters"; export { statusFilter, typeFilter } from "./target-table-filters"; +export { availabilitySorter, latencySorter, nameSorter, statusSorter } from "./target-table-sorters"; diff --git a/src/web/constants/target-table-filters.ts b/src/web/constants/target-table-filters.ts index 3997fa6..c52a9af 100644 --- a/src/web/constants/target-table-filters.ts +++ b/src/web/constants/target-table-filters.ts @@ -1,19 +1,19 @@ import type { PrimaryTableCol } from "tdesign-react"; export const statusFilter: PrimaryTableCol["filter"] = { - type: "single", list: [ { label: "全部", value: "" }, { label: "UP", value: "up" }, { label: "DOWN", value: "down" }, ], + type: "single", }; export const typeFilter: PrimaryTableCol["filter"] = { - type: "single", list: [ { label: "全部", value: "" }, { label: "HTTP", value: "http" }, { label: "CMD", value: "command" }, ], + type: "single", }; diff --git a/src/web/constants/target-table-sorters.ts b/src/web/constants/target-table-sorters.ts index 4438acb..8c24a09 100644 --- a/src/web/constants/target-table-sorters.ts +++ b/src/web/constants/target-table-sorters.ts @@ -5,15 +5,6 @@ const STATUS_ORDER: Record = { up: 1, }; -function getStatusRank(target: TargetStatus): number { - if (!target.latestCheck) return 2; - return target.latestCheck.matched ? STATUS_ORDER["up"]! : STATUS_ORDER["down"]!; -} - -export function statusSorter(a: TargetStatus, b: TargetStatus): number { - return getStatusRank(a) - getStatusRank(b); -} - export function availabilitySorter(a: TargetStatus, b: TargetStatus): number { return (a.stats?.availability ?? 0) - (b.stats?.availability ?? 0); } @@ -25,3 +16,12 @@ export function latencySorter(a: TargetStatus, b: TargetStatus): number { export function nameSorter(a: TargetStatus, b: TargetStatus): number { return a.name.localeCompare(b.name, "zh-CN"); } + +export function statusSorter(a: TargetStatus, b: TargetStatus): number { + return getStatusRank(a) - getStatusRank(b); +} + +function getStatusRank(target: TargetStatus): number { + if (!target.latestCheck) return 2; + return target.latestCheck.matched ? STATUS_ORDER["up"]! : STATUS_ORDER["down"]!; +} diff --git a/src/web/constants/target-type-display.ts b/src/web/constants/target-type-display.ts index 05e60fe..9baee87 100644 --- a/src/web/constants/target-type-display.ts +++ b/src/web/constants/target-type-display.ts @@ -1,6 +1,6 @@ export const TARGET_TYPE_DISPLAY = { - http: "HTTP", command: "CMD", + http: "HTTP", } as const; export type TargetType = keyof typeof TARGET_TYPE_DISPLAY; diff --git a/src/web/hooks/useTargetDetail.ts b/src/web/hooks/useTargetDetail.ts index 28d18b2..23789d0 100644 --- a/src/web/hooks/useTargetDetail.ts +++ b/src/web/hooks/useTargetDetail.ts @@ -1,34 +1,21 @@ -import { useCallback, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useCallback, useState } from "react"; + import type { HistoryResponse, SummaryResponse, TargetStatus, TrendPoint } from "../../shared/api"; + import { subtractHours } from "../utils/time"; const queryKeys = { + history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const, summary: () => ["summary"] as const, targets: () => ["targets"] as const, trend: (targetId: number, from: string, to: string) => ["trend", targetId, from, to] as const, - history: (targetId: number, from: string, to: string, page: number) => ["history", targetId, from, to, page] as const, }; -async function fetchJson(url: string): Promise { - const response = await fetch(url); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - return response.json() as Promise; -} - export function useSummary() { return useQuery({ - queryKey: queryKeys.summary(), queryFn: () => fetchJson("/api/summary"), - refetchInterval: 8000, - refetchIntervalInBackground: false, - }); -} - -export function useTargets() { - return useQuery({ - queryKey: queryKeys.targets(), - queryFn: () => fetchJson("/api/targets"), + queryKey: queryKeys.summary(), refetchInterval: 8000, refetchIntervalInBackground: false, }); @@ -36,7 +23,7 @@ export function useTargets() { export function useTargetDetail() { const queryClient = useQueryClient(); - const [selectedTargetId, setSelectedTargetId] = useState(null); + const [selectedTargetId, setSelectedTargetId] = useState(null); const [timeFrom, setTimeFrom] = useState(""); const [timeTo, setTimeTo] = useState(""); const [historyPage, setHistoryPage] = useState(1); @@ -47,27 +34,27 @@ export function useTargetDetail() { selectedTargetId !== null ? (targetsData?.find((t) => t.id === selectedTargetId) ?? null) : null; const trend = useQuery({ - queryKey: - selectedTargetId !== null && timeFrom && timeTo - ? queryKeys.trend(selectedTargetId, timeFrom, timeTo) - : ["trend", "disabled"], + enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, queryFn: () => fetchJson( `/api/targets/${selectedTargetId}/trend?from=${encodeURIComponent(timeFrom)}&to=${encodeURIComponent(timeTo)}`, ), - enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, + queryKey: + selectedTargetId !== null && timeFrom && timeTo + ? queryKeys.trend(selectedTargetId, timeFrom, timeTo) + : ["trend", "disabled"], }); const history = useQuery({ - queryKey: - selectedTargetId !== null && timeFrom && timeTo - ? queryKeys.history(selectedTargetId, timeFrom, timeTo, historyPage) - : ["history", "disabled"], + enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, queryFn: () => fetchJson( `/api/targets/${selectedTargetId}/history?from=${encodeURIComponent(timeFrom)}&to=${encodeURIComponent(timeTo)}&page=${historyPage}&pageSize=20`, ), - enabled: selectedTargetId !== null && !!timeFrom && !!timeTo, + queryKey: + selectedTargetId !== null && timeFrom && timeTo + ? queryKeys.history(selectedTargetId, timeFrom, timeTo, historyPage) + : ["history", "disabled"], }); const openDrawer = useCallback((target: TargetStatus) => { @@ -96,16 +83,31 @@ export function useTargetDetail() { }, []); return { - selectedTarget, - trendData: trend.data ?? [], - trendLoading: trend.isLoading, - historyData: history.data ?? { items: [], total: 0, page: 1, pageSize: 20 }, + closeDrawer, + handlePageChange, + handleTimeChange, + historyData: history.data ?? { items: [], page: 1, pageSize: 20, total: 0 }, historyLoading: history.isLoading, + openDrawer, + selectedTarget, timeFrom, timeTo, - openDrawer, - closeDrawer, - handleTimeChange, - handlePageChange, + trendData: trend.data ?? [], + trendLoading: trend.isLoading, }; } + +export function useTargets() { + return useQuery({ + queryFn: () => fetchJson("/api/targets"), + queryKey: queryKeys.targets(), + refetchInterval: 8000, + refetchIntervalInBackground: false, + }); +} + +async function fetchJson(url: string): Promise { + const response = await fetch(url); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json() as Promise; +} diff --git a/src/web/main.tsx b/src/web/main.tsx index 1e512aa..3dad36c 100644 --- a/src/web/main.tsx +++ b/src/web/main.tsx @@ -1,17 +1,19 @@ -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; + import { App } from "./app"; import "tdesign-react/dist/reset.css"; import "tdesign-react/dist/tdesign.min.css"; + import "./styles.css"; const queryClient = new QueryClient({ defaultOptions: { queries: { - retry: 1, refetchOnWindowFocus: true, + retry: 1, staleTime: 5000, }, }, diff --git a/tests/server/app.test.ts b/tests/server/app.test.ts index 74960cf..d1fb1a9 100644 --- a/tests/server/app.test.ts +++ b/tests/server/app.test.ts @@ -1,13 +1,15 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { createFetchHandler, type StaticAssets } from "../../src/server/app"; -import { ProbeStore } from "../../src/server/checker/store"; -import type { HistoryResponse, SummaryResponse, TargetStatus, HealthResponse } from "../../src/shared/api"; import { mkdir, rm } from "node:fs/promises"; -import { join } from "node:path"; import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import type { HealthResponse, HistoryResponse, SummaryResponse, TargetStatus } from "../../src/shared/api"; + +import { createFetchHandler, type StaticAssets } from "../../src/server/app"; import { checkerRegistry } from "../../src/server/checker/runner"; -import { HttpChecker } from "../../src/server/checker/runner/http/runner"; import { CommandChecker } from "../../src/server/checker/runner/command/runner"; +import { HttpChecker } from "../../src/server/checker/runner/http/runner"; +import { ProbeStore } from "../../src/server/checker/store"; function ensureRegistered() { if (!checkerRegistry.supportedTypes.includes("http")) { @@ -21,12 +23,12 @@ beforeAll(() => { }); const staticAssets: StaticAssets = { - indexHtml: new Blob(['DiAL
'], { - type: "text/html", - }), files: { "/assets/app.js": new Blob(["console.log('app');"], { type: "text/javascript" }), }, + indexHtml: new Blob(['DiAL
'], { + type: "text/html", + }), }; describe("API 路由", () => { @@ -40,57 +42,57 @@ describe("API 路由", () => { store = new ProbeStore(join(tempDir, "test.db")); store.syncTargets([ { - type: "http", - name: "test-a", group: "default", http: { - url: "http://a.com", - method: "GET", headers: {}, maxBodyBytes: 104857600, + method: "GET", + url: "http://a.com", }, intervalMs: 30000, + name: "test-a", timeoutMs: 10000, + type: "http", }, { - type: "command", - name: "test-b", - group: "default", command: { - exec: "echo", args: ["hello"], cwd: "/tmp", env: {}, + exec: "echo", maxOutputBytes: 104857600, }, + group: "default", intervalMs: 60000, + name: "test-b", timeoutMs: 5000, + type: "command", }, ]); const targets = store.getTargets(); store.insertCheckResult({ + durationMs: 150, + failure: null, + matched: true, + statusDetail: "200 OK", targetId: targets[0]!.id, timestamp: "2025-01-01T00:00:00.000Z", - matched: true, - durationMs: 150, - statusDetail: "200 OK", - failure: null, }); store.insertCheckResult({ + durationMs: null, + failure: { + actual: 500, + expected: 200, + kind: "error", + message: "状态码不匹配", + path: "$.status", + phase: "status", + }, + matched: false, + statusDetail: null, targetId: targets[0]!.id, timestamp: "2025-01-01T00:00:30.000Z", - matched: false, - durationMs: null, - statusDetail: null, - failure: { - kind: "error", - phase: "status", - path: "$.status", - expected: 200, - actual: 500, - message: "状态码不匹配", - }, }); fetchHandler = createFetchHandler({ mode: "test", staticAssets, store }); @@ -98,11 +100,11 @@ describe("API 路由", () => { afterAll(async () => { store.close(); - await rm(tempDir, { recursive: true, force: true }); + await rm(tempDir, { force: true, recursive: true }); }); test("/health 返回健康检查", async () => { - const response = await fetchHandler(new Request("http://localhost/health")); + const response = fetchHandler(new Request("http://localhost/health")); const body = (await response.json()) as HealthResponse; expect(response.status).toBe(200); @@ -111,7 +113,7 @@ describe("API 路由", () => { }); test("/api/summary 返回总览统计", async () => { - const response = await fetchHandler(new Request("http://localhost/api/summary")); + const response = fetchHandler(new Request("http://localhost/api/summary")); const body = (await response.json()) as SummaryResponse; expect(response.status).toBe(200); expect(body.total).toBe(2); @@ -122,7 +124,7 @@ describe("API 路由", () => { }); test("/api/targets 返回目标列表", async () => { - const response = await fetchHandler(new Request("http://localhost/api/targets")); + const response = fetchHandler(new Request("http://localhost/api/targets")); const body = (await response.json()) as TargetStatus[]; expect(response.status).toBe(200); @@ -150,7 +152,7 @@ describe("API 路由", () => { const targets = store.getTargets(); const from = "2024-01-01T00:00:00.000Z"; const to = "2026-12-31T23:59:59.999Z"; - const response = await fetchHandler( + const response = fetchHandler( new Request(`http://localhost/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}`), ); const body = (await response.json()) as HistoryResponse; @@ -168,7 +170,7 @@ describe("API 路由", () => { const targets = store.getTargets(); const from = "2024-01-01T00:00:00.000Z"; const to = "2026-12-31T23:59:59.999Z"; - const response = await fetchHandler( + const response = fetchHandler( new Request(`http://localhost/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}&pageSize=1`), ); const body = (await response.json()) as HistoryResponse; @@ -182,90 +184,92 @@ describe("API 路由", () => { const targets = store.getTargets(); const from = "2024-01-01T00:00:00.000Z"; const to = "2026-12-31T23:59:59.999Z"; - const response = await fetchHandler( + const response = fetchHandler( new Request(`http://localhost/api/targets/${targets[0]!.id}/trend?from=${from}&to=${to}`), ); - const body = await response.json(); + const body = (await response.json()) as unknown[]; expect(response.status).toBe(200); expect(Array.isArray(body)).toBe(true); }); test("查询不存在的目标返回 404", async () => { - const response = await fetchHandler( + const response = fetchHandler( new Request( "http://localhost/api/targets/99999/history?from=2024-01-01T00:00:00.000Z&to=2026-12-31T23:59:59.999Z", ), ); - const body = await response.json(); + const body = (await response.json()) as Record; expect(response.status).toBe(404); - expect(body.error).toBe("Target not found"); + expect(body["error"]).toBe("Target not found"); }); test("history 缺少 from/to 参数返回 400", async () => { const targets = store.getTargets(); - const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history`)); - const body = await response.json(); + const response = fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history`)); + const body = (await response.json()) as Record; expect(response.status).toBe(400); - expect(body.error).toContain("from and to"); + expect(body["error"]).toContain("from and to"); }); test("trend 缺少 from/to 参数返回 400", async () => { const targets = store.getTargets(); - const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/trend`)); - const body = await response.json(); + const response = fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/trend`)); + const body = (await response.json()) as Record; expect(response.status).toBe(400); - expect(body.error).toContain("from and to"); + expect(body["error"]).toContain("from and to"); }); - test("无效目标 ID 返回 400", async () => { - const response = await fetchHandler(new Request("http://localhost/api/targets/abc/history")); - const body = await response.json(); + test("trend 无效 targetId 返回 400", async () => { + const response = fetchHandler( + new Request("http://localhost/api/targets/invalid/trend?from=2024-01-01T00:00:00Z&to=2024-01-02T00:00:00Z"), + ); + const body = (await response.json()) as Record; expect(response.status).toBe(400); - expect(body.error).toBe("Invalid target ID"); + expect(body["error"]).toBe("Invalid target ID"); }); - test("未知 /api/* 返回 404", async () => { - const response = await fetchHandler(new Request("http://localhost/api/missing")); + test("未知 /api/* 返回 404", () => { + const response = fetchHandler(new Request("http://localhost/api/missing")); expect(response.status).toBe(404); }); test("HEAD 请求返回 headers 无 body", async () => { - const response = await fetchHandler(new Request("http://localhost/api/summary", { method: "HEAD" })); + const response = fetchHandler(new Request("http://localhost/api/summary", { method: "HEAD" })); const body = await response.text(); expect(response.status).toBe(200); expect(body).toBe(""); }); - test("不支持的 method 返回 405", async () => { - const response = await fetchHandler(new Request("http://localhost/api/summary", { method: "POST" })); + test("不支持的 method 返回 405", () => { + const response = fetchHandler(new Request("http://localhost/api/summary", { method: "POST" })); expect(response.status).toBe(405); expect(response.headers.get("allow")).toBe("GET, HEAD"); }); - test("生产响应包含安全 headers", async () => { + test("生产响应包含安全 headers", () => { const prodHandler = createFetchHandler({ mode: "production", staticAssets, store }); - const response = await prodHandler(new Request("http://localhost/api/summary")); + const response = prodHandler(new Request("http://localhost/api/summary")); expect(response.headers.get("x-content-type-options")).toBe("nosniff"); expect(response.headers.get("referrer-policy")).toBe("strict-origin-when-cross-origin"); }); - test("静态资源和 SPA fallback 正常工作", async () => { - const root = await fetchHandler(new Request("http://localhost/")); + test("静态资源和 SPA fallback 正常工作", () => { + const root = fetchHandler(new Request("http://localhost/")); expect(root.status).toBe(200); - const fallback = await fetchHandler(new Request("http://localhost/dashboard")); + const fallback = fetchHandler(new Request("http://localhost/dashboard")); expect(fallback.status).toBe(200); - const asset = await fetchHandler(new Request("http://localhost/assets/app.js")); + const asset = fetchHandler(new Request("http://localhost/assets/app.js")); expect(asset.status).toBe(200); }); @@ -274,12 +278,12 @@ describe("API 路由", () => { const t1Id = targets[0]!.id; store.insertCheckResult({ + durationMs: 100, + failure: { kind: "error", message: "test", path: "$", phase: "body" }, + matched: false, + statusDetail: "200 OK", targetId: t1Id, timestamp: "2025-06-01T00:00:00.000Z", - matched: false, - durationMs: 100, - statusDetail: "200 OK", - failure: { kind: "error", phase: "body", path: "$", message: "test" }, }); (store as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db @@ -288,9 +292,7 @@ describe("API 路由", () => { const from = "2025-06-01T00:00:00.000Z"; const to = "2025-06-01T23:59:59.999Z"; - const response = await fetchHandler( - new Request(`http://localhost/api/targets/${t1Id}/history?from=${from}&to=${to}`), - ); + const response = fetchHandler(new Request(`http://localhost/api/targets/${t1Id}/history?from=${from}&to=${to}`)); const body = (await response.json()) as HistoryResponse; expect(response.status).toBe(200); diff --git a/tests/server/checker/config-loader.test.ts b/tests/server/checker/config-loader.test.ts index 8386e29..9a7977f 100644 --- a/tests/server/checker/config-loader.test.ts +++ b/tests/server/checker/config-loader.test.ts @@ -1,12 +1,13 @@ -import { beforeAll, afterAll, describe, expect, test } from "bun:test"; -import { loadConfig, parseDuration } from "../../../src/server/checker/config-loader"; -import { readRuntimeConfig } from "../../../src/server/config"; +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { mkdir, rm, writeFile } from "node:fs/promises"; -import { join } from "node:path"; import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { loadConfig, parseDuration } from "../../../src/server/checker/config-loader"; import { checkerRegistry } from "../../../src/server/checker/runner"; -import { HttpChecker } from "../../../src/server/checker/runner/http/runner"; import { CommandChecker } from "../../../src/server/checker/runner/command/runner"; +import { HttpChecker } from "../../../src/server/checker/runner/http/runner"; +import { readRuntimeConfig } from "../../../src/server/config"; function ensureRegistered() { if (!checkerRegistry.supportedTypes.includes("http")) { @@ -66,7 +67,7 @@ describe("loadConfig", () => { }); afterAll(async () => { - await rm(tempDir, { recursive: true, force: true }); + await rm(tempDir, { force: true, recursive: true }); }); test("解析最简 HTTP 配置", async () => { @@ -125,7 +126,7 @@ describe("loadConfig", () => { expect(t.command.args).toEqual(["nginx"]); expect(t.command.cwd).toBe(subdir); expect(t.command.maxOutputBytes).toBe(104857600); - expect(t.command.env.PATH).toBeDefined(); + expect(t.command.env["PATH"]).toBeDefined(); } }); @@ -230,6 +231,7 @@ targets: }); test("配置文件不存在抛出错误", async () => { + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig("/nonexistent/file.yaml")).rejects.toThrow("配置文件不存在"); }); @@ -243,6 +245,7 @@ targets: url: "http://example.com" `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("缺少 name 字段"); }); @@ -256,6 +259,7 @@ targets: url: "http://example.com" `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("缺少 type 字段"); }); @@ -269,6 +273,7 @@ targets: http: {} `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("缺少 http.url 字段"); }); @@ -282,6 +287,7 @@ targets: command: {} `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("缺少 command.exec 字段"); }); @@ -294,6 +300,7 @@ targets: type: dns `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("不支持的 type"); }); @@ -312,12 +319,14 @@ targets: url: "http://b.com" `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("target name 重复"); }); test("targets 为空数组抛出错误", async () => { const configPath = join(tempDir, "empty-targets.yaml"); await writeFile(configPath, `targets: []`); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("至少一个 target"); }); @@ -334,6 +343,7 @@ targets: url: "http://a.com" `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("无效端口号"); }); @@ -350,6 +360,7 @@ targets: url: "http://a.com" `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("maxConcurrentChecks 必须为正整数"); }); @@ -367,6 +378,7 @@ targets: url: "http://a.com" `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("无效的 size 格式"); }); @@ -382,6 +394,7 @@ targets: url: "http://a.com" `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("无效的时长格式"); }); @@ -409,9 +422,9 @@ targets: const t = config.targets[0]!; if (t.type === "http") { expect(t.expect).toEqual({ - status: [200, 201], - body: [{ contains: "ok" }, { json: { path: "$.status", equals: "ok" } }], + body: [{ contains: "ok" }, { json: { equals: "ok", path: "$.status" } }], maxDurationMs: 3000, + status: [200, 201], }); } }); @@ -441,9 +454,9 @@ targets: if (t.type === "command") { expect(t.expect).toEqual({ exitCode: [0, 2], - stdout: [{ contains: "ok" }, { match: "done" }], - stderr: [{ empty: true }], maxDurationMs: 5000, + stderr: [{ empty: true }], + stdout: [{ contains: "ok" }, { match: "done" }], }); } }); @@ -488,9 +501,9 @@ targets: const config = await loadConfig(configPath); const t = config.targets[0]!; if (t.type === "command") { - expect(t.command.env.LANG).toBe("C"); - expect(t.command.env.CUSTOM_VAR).toBe("test"); - expect(t.command.env.PATH).toBeDefined(); + expect(t.command.env["LANG"]).toBe("C"); + expect(t.command.env["CUSTOM_VAR"]).toBe("test"); + expect(t.command.env["PATH"]).toBeDefined(); } }); @@ -540,6 +553,7 @@ targets: `, ); + // eslint-disable-next-line @typescript-eslint/await-thenable await expect(loadConfig(configPath)).rejects.toThrow("group 字段必须为字符串"); }); }); diff --git a/tests/server/checker/engine.test.ts b/tests/server/checker/engine.test.ts index 9d498b5..9d57498 100644 --- a/tests/server/checker/engine.test.ts +++ b/tests/server/checker/engine.test.ts @@ -1,10 +1,38 @@ import { describe, expect, test } from "bun:test"; -import { ProbeEngine } from "../../../src/server/checker/engine"; + import type { ProbeStore } from "../../../src/server/checker/store"; import type { ResolvedCommandTarget, ResolvedHttpTarget, ResolvedTarget } from "../../../src/server/checker/types"; + +import { ProbeEngine } from "../../../src/server/checker/engine"; import { checkerRegistry } from "../../../src/server/checker/runner"; -import { HttpChecker } from "../../../src/server/checker/runner/http/runner"; import { CommandChecker } from "../../../src/server/checker/runner/command/runner"; +import { HttpChecker } from "../../../src/server/checker/runner/http/runner"; + +function createMockStore(targetNames: string[]) { + let nextId = 1; + const targets = targetNames.map((name) => ({ id: nextId++, name })); + const results: Array> = []; + + return { + _results: results, + getTargets() { + return targets.map(({ id, name }) => ({ + config: "", + expect: null, + grp: "default", + id, + interval_ms: 60000, + name, + target: "", + timeout_ms: 5000, + type: "command" as const, + })); + }, + insertCheckResult(result: Record) { + results.push(result); + }, + }; +} function ensureRegistered() { if (!checkerRegistry.supportedTypes.includes("http")) { @@ -13,46 +41,20 @@ function ensureRegistered() { } } -function createMockStore(targetNames: string[]) { - let nextId = 1; - const targets = targetNames.map((name) => ({ id: nextId++, name })); - const results: Array> = []; - - return { - getTargets() { - return targets.map(({ id, name }) => ({ - id, - name, - type: "command" as const, - target: "", - config: "", - interval_ms: 60000, - timeout_ms: 5000, - expect: null, - grp: "default", - })); - }, - insertCheckResult(result: Record) { - results.push(result); - }, - _results: results, - }; -} - function makeCommandTarget(name: string, overrides?: Partial): ResolvedCommandTarget { return { - type: "command", - name, - group: "default", command: { - exec: "echo", args: ["hello"], cwd: "/tmp", env: {}, + exec: "echo", maxOutputBytes: 1024 * 1024, }, + group: "default", intervalMs: 60000, + name, timeoutMs: 5000, + type: "command", ...overrides, }; } @@ -80,16 +82,16 @@ describe("ProbeEngine", () => { const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(1); - expect(results[0]!.matched).toBe(true); - expect(results[0]!.statusDetail).toBe("exitCode=0"); + expect(results[0]!["matched"]).toBe(true); + expect(results[0]!["statusDetail"]).toBe("exitCode=0"); }); test("多个目标并发执行", async () => { const targetA = makeCommandTarget("echo-a", { - command: { exec: "echo", args: ["a"], cwd: "/tmp", env: {}, maxOutputBytes: 1024 * 1024 }, + command: { args: ["a"], cwd: "/tmp", env: {}, exec: "echo", maxOutputBytes: 1024 * 1024 }, }); const targetB = makeCommandTarget("echo-b", { - command: { exec: "echo", args: ["b"], cwd: "/tmp", env: {}, maxOutputBytes: 1024 * 1024 }, + command: { args: ["b"], cwd: "/tmp", env: {}, exec: "echo", maxOutputBytes: 1024 * 1024 }, }); const mockStore = createMockStore(["echo-a", "echo-b"]) as unknown as ProbeStore; @@ -106,7 +108,7 @@ describe("ProbeEngine", () => { test("失败目标不阻塞其他目标", async () => { const badTarget = makeCommandTarget("bad-cmd", { - command: { exec: "false", args: [], cwd: "/tmp", env: {}, maxOutputBytes: 1024 * 1024 }, + command: { args: [], cwd: "/tmp", env: {}, exec: "false", maxOutputBytes: 1024 * 1024 }, }); const goodTarget = makeCommandTarget("good-cmd"); @@ -121,8 +123,8 @@ describe("ProbeEngine", () => { const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(2); - const badResult = results.find((r) => r.matched === false); - const goodResult = results.find((r) => r.matched === true); + const badResult = results.find((r) => r["matched"] === false); + const goodResult = results.find((r) => r["matched"] === true); expect(badResult).toBeDefined(); expect(goodResult).toBeDefined(); }); @@ -130,7 +132,7 @@ describe("ProbeEngine", () => { test("并发限制 maxConcurrentChecks", async () => { const targets = Array.from({ length: 5 }, (_, i) => makeCommandTarget(`cmd-${i}`, { - command: { exec: "echo", args: [String(i)], cwd: "/tmp", env: {}, maxOutputBytes: 1024 * 1024 }, + command: { args: [String(i)], cwd: "/tmp", env: {}, exec: "echo", maxOutputBytes: 1024 * 1024 }, }), ); @@ -145,7 +147,7 @@ describe("ProbeEngine", () => { const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(5); for (const r of results) { - expect(r.matched).toBe(true); + expect(r["matched"]).toBe(true); } }); @@ -177,25 +179,25 @@ describe("ProbeEngine", () => { test("HTTP 目标运行", async () => { const httpServer = Bun.serve({ - port: 0, fetch() { return new Response("ok"); }, + port: 0, }); try { const httpTarget: ResolvedHttpTarget = { - type: "http", - name: "http-test", group: "default", http: { - url: `http://localhost:${httpServer.port}/`, - method: "GET", headers: {}, maxBodyBytes: 1024 * 1024, + method: "GET", + url: `http://localhost:${httpServer.port}/`, }, intervalMs: 60000, + name: "http-test", timeoutMs: 5000, + type: "http", }; const mockStore = createMockStore(["http-test"]) as unknown as ProbeStore; @@ -208,10 +210,10 @@ describe("ProbeEngine", () => { const results = (mockStore as unknown as { _results: Array> })._results; expect(results.length).toBe(1); - expect(results[0]!.matched).toBe(true); - expect(results[0]!.statusDetail).toBe("HTTP 200"); + expect(results[0]!["matched"]).toBe(true); + expect(results[0]!["statusDetail"]).toBe("HTTP 200"); } finally { - httpServer.stop(); + void httpServer.stop(); } }); }); diff --git a/tests/server/checker/runner/command/expect.test.ts b/tests/server/checker/runner/command/expect.test.ts index 59629dc..fbdee9e 100644 --- a/tests/server/checker/runner/command/expect.test.ts +++ b/tests/server/checker/runner/command/expect.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import { checkExitCode } from "../../../../../src/server/checker/runner/command/expect"; describe("checkExitCode", () => { diff --git a/tests/server/checker/runner/command/runner.test.ts b/tests/server/checker/runner/command/runner.test.ts index ef354a6..5d442f0 100644 --- a/tests/server/checker/runner/command/runner.test.ts +++ b/tests/server/checker/runner/command/runner.test.ts @@ -1,31 +1,11 @@ import { describe, expect, test } from "bun:test"; -import { CommandChecker } from "../../../../../src/server/checker/runner/command/runner"; + import type { CheckerContext } from "../../../../../src/server/checker/runner/types"; import type { ResolvedCommandTarget } from "../../../../../src/server/checker/types"; -const checker = new CommandChecker(); +import { CommandChecker } from "../../../../../src/server/checker/runner/command/runner"; -function makeTarget( - command: Partial, - overrides?: Partial, -): ResolvedCommandTarget { - return { - type: "command", - name: "test-cmd", - group: "default", - command: { - exec: "echo", - args: ["hello"], - cwd: "/tmp", - env: {}, - maxOutputBytes: 1024 * 1024, - ...command, - }, - intervalMs: 60000, - timeoutMs: 5000, - ...overrides, - }; -} +const checker = new CommandChecker(); function makeCtx(timeoutMs = 5000): CheckerContext { const controller = new AbortController(); @@ -33,23 +13,48 @@ function makeCtx(timeoutMs = 5000): CheckerContext { return { signal: controller.signal }; } +function makeTarget( + command: Partial, + overrides?: Partial, +): ResolvedCommandTarget { + return { + command: { + args: ["hello"], + cwd: "/tmp", + env: {}, + exec: "echo", + maxOutputBytes: 1024 * 1024, + ...command, + }, + group: "default", + intervalMs: 60000, + name: "test-cmd", + timeoutMs: 5000, + type: "command", + ...overrides, + }; +} + describe("CommandChecker", () => { test("exitCode=0 成功", async () => { - const result = await checker.execute(makeTarget({ exec: "true", args: [] }), makeCtx()); + const result = await checker.execute(makeTarget({ args: [], exec: "true" }), makeCtx()); expect(result.matched).toBe(true); expect(result.statusDetail).toBe("exitCode=0"); expect(result.failure).toBeNull(); }); test("exitCode=1 不匹配默认 [0]", async () => { - const result = await checker.execute(makeTarget({ exec: "false", args: [] }), makeCtx()); + const result = await checker.execute(makeTarget({ args: [], exec: "false" }), makeCtx()); expect(result.matched).toBe(false); expect(result.statusDetail).toBe("exitCode=1"); expect(result.failure!.phase).toBe("exitCode"); }); test("exitCode=1 匹配自定义 [1]", async () => { - const result = await checker.execute(makeTarget({ exec: "false", args: [] }, { expect: { exitCode: [1] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ args: [], exec: "false" }, { expect: { exitCode: [1] } }), + makeCtx(), + ); expect(result.matched).toBe(true); expect(result.statusDetail).toBe("exitCode=1"); }); @@ -62,54 +67,69 @@ describe("CommandChecker", () => { }); test("超时返回错误", async () => { - const result = await checker.execute(makeTarget({ exec: "sleep", args: ["10"] }, { timeoutMs: 100 }), makeCtx(100)); + const result = await checker.execute(makeTarget({ args: ["10"], exec: "sleep" }, { timeoutMs: 100 }), makeCtx(100)); expect(result.matched).toBe(false); expect(result.failure!.message).toContain("超时"); }); test("stdout 输出捕获", async () => { - const result = await checker.execute(makeTarget({ exec: "echo", args: ["hello world"] }), makeCtx()); + const result = await checker.execute(makeTarget({ args: ["hello world"], exec: "echo" }), makeCtx()); expect(result.matched).toBe(true); }); test("stdout 匹配 expect", async () => { - const result = await checker.execute(makeTarget({ exec: "echo", args: ["hello"] }, { expect: { stdout: [{ contains: "hello" }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ args: ["hello"], exec: "echo" }, { expect: { stdout: [{ contains: "hello" }] } }), + makeCtx(), + ); expect(result.matched).toBe(true); }); test("stdout 不匹配 expect", async () => { - const result = await checker.execute(makeTarget({ exec: "echo", args: ["hello"] }, { expect: { stdout: [{ contains: "nonexistent" }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ args: ["hello"], exec: "echo" }, { expect: { stdout: [{ contains: "nonexistent" }] } }), + makeCtx(), + ); expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("stdout"); }); test("stderr 匹配 expect", async () => { - const result = await checker.execute(makeTarget({ exec: "bash", args: ["-c", "echo error >&2"] }, { expect: { stderr: [{ contains: "error" }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ args: ["-c", "echo error >&2"], exec: "bash" }, { expect: { stderr: [{ contains: "error" }] } }), + makeCtx(), + ); expect(result.matched).toBe(true); }); test("输出超过 maxOutputBytes", async () => { - const result = await checker.execute(makeTarget({ exec: "bash", args: ["-c", "yes | head -1000"], maxOutputBytes: 10 }), makeCtx()); + const result = await checker.execute( + makeTarget({ args: ["-c", "yes | head -1000"], exec: "bash", maxOutputBytes: 10 }), + makeCtx(), + ); expect(result.matched).toBe(false); expect(result.failure!.message).toContain("超过限制"); }); test("durationMs 非空", async () => { - const result = await checker.execute(makeTarget({ exec: "true", args: [] }), makeCtx()); + const result = await checker.execute(makeTarget({ args: [], exec: "true" }), makeCtx()); expect(result.durationMs).not.toBeNull(); expect(result.durationMs!).toBeGreaterThanOrEqual(0); }); test("不使用 shell,通配符不被展开", async () => { - const result = await checker.execute(makeTarget({ exec: "echo", args: ["*"] }, { expect: { stdout: [{ contains: "*" }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ args: ["*"], exec: "echo" }, { expect: { stdout: [{ contains: "*" }] } }), + makeCtx(), + ); expect(result.matched).toBe(true); }); test("serialize 返回命令摘要和 config JSON", () => { - const target = makeTarget({ exec: "echo", args: ["hello"] }); + const target = makeTarget({ args: ["hello"], exec: "echo" }); const s = checker.serialize(target); expect(s.target).toBe("exec echo hello"); - const config = JSON.parse(s.config); + const config = JSON.parse(s.config) as { args: string[]; exec: string }; expect(config.exec).toBe("echo"); expect(config.args).toEqual(["hello"]); }); diff --git a/tests/server/checker/runner/http/expect.test.ts b/tests/server/checker/runner/http/expect.test.ts index e08d9e6..3ed139f 100644 --- a/tests/server/checker/runner/http/expect.test.ts +++ b/tests/server/checker/runner/http/expect.test.ts @@ -1,18 +1,21 @@ import { describe, expect, test } from "bun:test"; + import { checkHttpExpect } from "../../../../../src/server/checker/runner/http/expect"; -function obs(overrides: { statusCode?: number; headers?: Record; body?: string | null; durationMs?: number } = {}) { +function obs( + overrides: { body?: null | string; durationMs?: number; headers?: Record; statusCode?: number } = {}, +) { return { - statusCode: overrides.statusCode ?? 200, - headers: overrides.headers ?? {}, body: overrides.body ?? "", durationMs: overrides.durationMs ?? 100, + headers: overrides.headers ?? {}, + statusCode: overrides.statusCode ?? 200, }; } describe("checkHttpExpect", () => { test("无 expect 配置时默认检查 status [200] 匹配成功", () => { - const r = checkHttpExpect(obs().statusCode, obs().headers, obs().body as string, obs().durationMs); + const r = checkHttpExpect(obs().statusCode, obs().headers, obs().body, obs().durationMs); expect(r.matched).toBe(true); expect(r.failure).toBeNull(); }); @@ -65,7 +68,9 @@ describe("checkHttpExpect", () => { test("headers 操作符格式检查", () => { const h = { "content-type": "application/json" }; expect(checkHttpExpect(200, h, "", 100, { headers: { "content-type": { contains: "json" } } }).matched).toBe(true); - expect(checkHttpExpect(200, h, "", 100, { headers: { "content-type": { match: "^application/" } } }).matched).toBe(true); + expect(checkHttpExpect(200, h, "", 100, { headers: { "content-type": { match: "^application/" } } }).matched).toBe( + true, + ); expect(checkHttpExpect(200, h, "", 100, { headers: { "content-type": { contains: "xml" } } }).matched).toBe(false); }); @@ -81,9 +86,9 @@ describe("checkHttpExpect", () => { }); test("body 规则数组按顺序检查", () => { - const body = JSON.stringify({ status: "ok", count: 5 }); + const body = JSON.stringify({ count: 5, status: "ok" }); const r = checkHttpExpect(200, {}, body, 100, { - body: [{ contains: "ok" }, { json: { path: "$.count", gte: 1 } }], + body: [{ contains: "ok" }, { json: { gte: 1, path: "$.count" } }], }); expect(r.matched).toBe(true); }); @@ -104,26 +109,26 @@ describe("checkHttpExpect", () => { test("完整流水线 status->duration->headers->body 全部通过", () => { const r = checkHttpExpect(200, { "content-type": "application/json" }, JSON.stringify({ status: "healthy" }), 50, { - status: [200], - maxDurationMs: 100, + body: [{ json: { equals: "healthy", path: "$.status" } }], headers: { "content-type": { contains: "json" } }, - body: [{ json: { path: "$.status", equals: "healthy" } }], + maxDurationMs: 100, + status: [200], }); expect(r.matched).toBe(true); expect(r.failure).toBeNull(); }); test("完整流水线 status 通过但 duration 失败", () => { - const r = checkHttpExpect(200, {}, "", 500, { status: [200], maxDurationMs: 100 }); + const r = checkHttpExpect(200, {}, "", 500, { maxDurationMs: 100, status: [200] }); expect(r.matched).toBe(false); expect(r.failure!.phase).toBe("duration"); }); test("完整流水线 status 和 duration 通过但 headers 失败", () => { const r = checkHttpExpect(200, { "x-api": "v1" }, "", 50, { - status: [200], - maxDurationMs: 100, headers: { "x-api": "v2" }, + maxDurationMs: 100, + status: [200], }); expect(r.matched).toBe(false); expect(r.failure!.phase).toBe("headers"); @@ -131,10 +136,10 @@ describe("checkHttpExpect", () => { test("完整流水线 status/duration/headers 通过但 body 失败", () => { const r = checkHttpExpect(200, { "content-type": "text/plain" }, "error occurred", 50, { - status: [200], - maxDurationMs: 100, - headers: { "content-type": "text/plain" }, body: [{ contains: "success" }], + headers: { "content-type": "text/plain" }, + maxDurationMs: 100, + status: [200], }); expect(r.matched).toBe(false); expect(r.failure!.phase).toBe("body"); diff --git a/tests/server/checker/runner/http/runner.test.ts b/tests/server/checker/runner/http/runner.test.ts index 161bfeb..dc8572e 100644 --- a/tests/server/checker/runner/http/runner.test.ts +++ b/tests/server/checker/runner/http/runner.test.ts @@ -1,8 +1,10 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { HttpChecker } from "../../../../../src/server/checker/runner/http/runner"; + import type { CheckerContext } from "../../../../../src/server/checker/runner/types"; import type { ResolvedHttpTarget } from "../../../../../src/server/checker/types"; +import { HttpChecker } from "../../../../../src/server/checker/runner/http/runner"; + const checker = new HttpChecker(); describe("HttpChecker", () => { @@ -11,61 +13,61 @@ describe("HttpChecker", () => { beforeAll(() => { server = Bun.serve({ - port: 0, fetch(req) { const url = new URL(req.url); switch (url.pathname) { - case "/ok": - return new Response("hello world", { - headers: { "content-type": "text/plain", "x-custom": "test-value" }, + case "/echo": + return new Response(JSON.stringify({ body: req.body ? "present" : "empty", method: req.method }), { + headers: { "content-type": "application/json" }, }); case "/json": return new Response(JSON.stringify({ status: "ok" }), { headers: { "content-type": "application/json" }, }); - case "/echo": - return new Response(JSON.stringify({ method: req.method, body: req.body ? "present" : "empty" }), { - headers: { "content-type": "application/json" }, - }); case "/large": return new Response("x".repeat(2000)); case "/notfound": return new Response("not found", { status: 404 }); + case "/ok": + return new Response("hello world", { + headers: { "content-type": "text/plain", "x-custom": "test-value" }, + }); default: return new Response("ok"); } }, + port: 0, }); baseUrl = `http://localhost:${server.port}`; }); afterAll(() => { - server.stop(); + void server.stop(); }); function makeTarget(overrides: { - url?: string; - method?: string; body?: string; - headers?: Record; expect?: Record; + headers?: Record; maxBodyBytes?: number; + method?: string; timeoutMs?: number; + url?: string; }): ResolvedHttpTarget { return { - type: "http", - name: "test-http", + expect: overrides.expect, group: "default", http: { - url: overrides.url ?? `${baseUrl}/ok`, - method: overrides.method ?? "GET", - headers: overrides.headers ?? {}, body: overrides.body, + headers: overrides.headers ?? {}, maxBodyBytes: overrides.maxBodyBytes ?? 1024 * 1024, + method: overrides.method ?? "GET", + url: overrides.url ?? `${baseUrl}/ok`, }, intervalMs: 60000, + name: "test-http", timeoutMs: overrides.timeoutMs ?? 5000, - expect: overrides.expect as ResolvedHttpTarget["expect"], + type: "http", }; } @@ -91,39 +93,60 @@ describe("HttpChecker", () => { }); test("404 匹配自定义 status [404]", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/notfound`, expect: { status: [404] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ expect: { status: [404] }, url: `${baseUrl}/notfound` }), + makeCtx(), + ); expect(result.matched).toBe(true); }); test("headers 检查通过", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/ok`, expect: { headers: { "x-custom": "test-value" } } }), makeCtx()); + const result = await checker.execute( + makeTarget({ expect: { headers: { "x-custom": "test-value" } }, url: `${baseUrl}/ok` }), + makeCtx(), + ); expect(result.matched).toBe(true); }); test("headers 检查失败", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/ok`, expect: { headers: { "x-custom": "wrong-value" } } }), makeCtx()); + const result = await checker.execute( + makeTarget({ expect: { headers: { "x-custom": "wrong-value" } }, url: `${baseUrl}/ok` }), + makeCtx(), + ); expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("headers"); }); test("body contains 检查", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/ok`, expect: { body: [{ contains: "hello" }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ expect: { body: [{ contains: "hello" }] }, url: `${baseUrl}/ok` }), + makeCtx(), + ); expect(result.matched).toBe(true); }); test("body contains 失败", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/ok`, expect: { body: [{ contains: "nonexistent" }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ expect: { body: [{ contains: "nonexistent" }] }, url: `${baseUrl}/ok` }), + makeCtx(), + ); expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("body"); }); test("body json 检查", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/json`, expect: { body: [{ json: { path: "$.status", equals: "ok" } }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ expect: { body: [{ json: { equals: "ok", path: "$.status" } }] }, url: `${baseUrl}/json` }), + makeCtx(), + ); expect(result.matched).toBe(true); }); test("响应体超过 maxBodyBytes", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/large`, maxBodyBytes: 100, expect: { body: [{ contains: "x" }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ expect: { body: [{ contains: "x" }] }, maxBodyBytes: 100, url: `${baseUrl}/large` }), + makeCtx(), + ); expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("body"); expect(result.failure!.message).toContain("超过限制"); @@ -131,41 +154,59 @@ describe("HttpChecker", () => { test("请求超时", async () => { const timeoutServer = Bun.serve({ - port: 0, async fetch() { await new Promise((resolve) => setTimeout(resolve, 10000)); return new Response("late"); }, + port: 0, }); try { - const result = await checker.execute(makeTarget({ url: `http://localhost:${timeoutServer.port}/`, timeoutMs: 100 }), makeCtx(100)); + const result = await checker.execute( + makeTarget({ timeoutMs: 100, url: `http://localhost:${timeoutServer.port}/` }), + makeCtx(100), + ); expect(result.matched).toBe(false); expect(result.failure!.message).toContain("超时"); } finally { - timeoutServer.stop(); + void timeoutServer.stop(); } }); test("快速失败:status 失败时不读取 body", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/notfound`, expect: { status: [200], body: [{ contains: "something" }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ expect: { body: [{ contains: "something" }], status: [200] }, url: `${baseUrl}/notfound` }), + makeCtx(), + ); expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("status"); }); test("status 通过但 body 失败", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/ok`, expect: { status: [200], body: [{ contains: "not-in-body" }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ expect: { body: [{ contains: "not-in-body" }], status: [200] }, url: `${baseUrl}/ok` }), + makeCtx(), + ); expect(result.matched).toBe(false); expect(result.failure!.phase).toBe("body"); }); test("无 expect 时默认检查 status 200", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/ok`, expect: undefined }), makeCtx()); + const result = await checker.execute(makeTarget({ expect: undefined, url: `${baseUrl}/ok` }), makeCtx()); expect(result.matched).toBe(true); }); test("POST 请求携带 body", async () => { - const result = await checker.execute(makeTarget({ url: `${baseUrl}/echo`, method: "POST", body: "test-body", headers: { "content-type": "text/plain" }, expect: { status: [200], body: [{ json: { path: "$.body", equals: "present" } }] } }), makeCtx()); + const result = await checker.execute( + makeTarget({ + body: "test-body", + expect: { body: [{ json: { equals: "present", path: "$.body" } }], status: [200] }, + headers: { "content-type": "text/plain" }, + method: "POST", + url: `${baseUrl}/echo`, + }), + makeCtx(), + ); expect(result.matched).toBe(true); }); @@ -173,7 +214,7 @@ describe("HttpChecker", () => { const target = makeTarget({}); const s = checker.serialize(target); expect(s.target).toBe(target.http.url); - const config = JSON.parse(s.config); + const config = JSON.parse(s.config) as { method: string; url: string }; expect(config.url).toBe(target.http.url); expect(config.method).toBe("GET"); }); diff --git a/tests/server/checker/runner/registry.test.ts b/tests/server/checker/runner/registry.test.ts index 7b64130..090fa46 100644 --- a/tests/server/checker/runner/registry.test.ts +++ b/tests/server/checker/runner/registry.test.ts @@ -1,13 +1,16 @@ import { describe, expect, test } from "bun:test"; -import { CheckerRegistry } from "../../../../src/server/checker/runner/registry"; + import type { Checker } from "../../../../src/server/checker/runner/types"; +import type { CheckResult, ResolvedTarget } from "../../../../src/server/checker/types"; + +import { CheckerRegistry } from "../../../../src/server/checker/runner/registry"; function createChecker(type: string): Checker { return { + execute: () => Promise.resolve({} as unknown as CheckResult), + resolve: () => ({}) as unknown as ResolvedTarget, + serialize: () => ({ config: "", target: "" }), type, - resolve: () => ({}) as any, - execute: () => Promise.resolve({} as any), - serialize: () => ({ target: "", config: "" }), }; } diff --git a/tests/server/checker/runner/shared/body.test.ts b/tests/server/checker/runner/shared/body.test.ts index 379858c..2d74e35 100644 --- a/tests/server/checker/runner/shared/body.test.ts +++ b/tests/server/checker/runner/shared/body.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import { checkBodyExpect } from "../../../../../src/server/checker/runner/shared/body"; describe("checkBodyExpect (BodyRule[])", () => { @@ -41,89 +42,89 @@ describe("checkBodyExpect (BodyRule[])", () => { }); test("json 等值匹配成功", () => { - const body = JSON.stringify({ status: "ok", code: 0 }); - const r = checkBodyExpect(body, [{ json: { path: "$.status", equals: "ok" } }]); + const body = JSON.stringify({ code: 0, status: "ok" }); + const r = checkBodyExpect(body, [{ json: { equals: "ok", path: "$.status" } }]); expect(r.matched).toBe(true); }); test("json 等值匹配失败", () => { const body = JSON.stringify({ status: "ok" }); - const r = checkBodyExpect(body, [{ json: { path: "$.status", equals: "error" } }]); + const r = checkBodyExpect(body, [{ json: { equals: "error", path: "$.status" } }]); expect(r.matched).toBe(false); expect(r.failure!.kind).toBe("mismatch"); }); test("json 操作符匹配", () => { const body = JSON.stringify({ count: 42, version: "v2.1.0" }); - expect(checkBodyExpect(body, [{ json: { path: "$.count", gte: 10 } }]).matched).toBe(true); - expect(checkBodyExpect(body, [{ json: { path: "$.version", match: "\\d+\\.\\d+\\.\\d+" } }]).matched).toBe(true); - expect(checkBodyExpect(body, [{ json: { path: "$.count", gte: 100 } }]).matched).toBe(false); + expect(checkBodyExpect(body, [{ json: { gte: 10, path: "$.count" } }]).matched).toBe(true); + expect(checkBodyExpect(body, [{ json: { match: "\\d+\\.\\d+\\.\\d+", path: "$.version" } }]).matched).toBe(true); + expect(checkBodyExpect(body, [{ json: { gte: 100, path: "$.count" } }]).matched).toBe(false); }); test("json 路径不存在", () => { const body = JSON.stringify({ status: "ok" }); - const r = checkBodyExpect(body, [{ json: { path: "$.notExist", equals: "value" } }]); + const r = checkBodyExpect(body, [{ json: { equals: "value", path: "$.notExist" } }]); expect(r.matched).toBe(false); }); test("json 解析失败", () => { - const r = checkBodyExpect("not json", [{ json: { path: "$.status", equals: "ok" } }]); + const r = checkBodyExpect("not json", [{ json: { equals: "ok", path: "$.status" } }]); expect(r.matched).toBe(false); expect(r.failure!.kind).toBe("error"); }); test("css 文本内容匹配", () => { const html = "
OK
1.0"; - expect(checkBodyExpect(html, [{ css: { selector: "div#health", equals: "OK" } }]).matched).toBe(true); - expect(checkBodyExpect(html, [{ css: { selector: "span.ver", equals: "1.0" } }]).matched).toBe(true); - expect(checkBodyExpect(html, [{ css: { selector: "div#health", equals: "ERROR" } }]).matched).toBe(false); + expect(checkBodyExpect(html, [{ css: { equals: "OK", selector: "div#health" } }]).matched).toBe(true); + expect(checkBodyExpect(html, [{ css: { equals: "1.0", selector: "span.ver" } }]).matched).toBe(true); + expect(checkBodyExpect(html, [{ css: { equals: "ERROR", selector: "div#health" } }]).matched).toBe(false); }); test("css 选择器无匹配元素", () => { const html = "
OK
"; - const r = checkBodyExpect(html, [{ css: { selector: "span.missing", equals: "OK" } }]); + const r = checkBodyExpect(html, [{ css: { equals: "OK", selector: "span.missing" } }]); expect(r.matched).toBe(false); }); test("css attr 提取", () => { const html = ''; expect( - checkBodyExpect(html, [{ css: { selector: 'meta[name="version"]', attr: "content", equals: "2.0.1" } }]).matched, + checkBodyExpect(html, [{ css: { attr: "content", equals: "2.0.1", selector: 'meta[name="version"]' } }]).matched, ).toBe(true); }); test("css exists 检查", () => { const html = "
OK
"; - expect(checkBodyExpect(html, [{ css: { selector: "div#test", exists: true } }]).matched).toBe(true); - expect(checkBodyExpect(html, [{ css: { selector: "span#missing", exists: false } }]).matched).toBe(true); - expect(checkBodyExpect(html, [{ css: { selector: "div#test", exists: false } }]).matched).toBe(false); + expect(checkBodyExpect(html, [{ css: { exists: true, selector: "div#test" } }]).matched).toBe(true); + expect(checkBodyExpect(html, [{ css: { exists: false, selector: "span#missing" } }]).matched).toBe(true); + expect(checkBodyExpect(html, [{ css: { exists: false, selector: "div#test" } }]).matched).toBe(false); }); test("xpath 节点文本匹配", () => { const xml = "ok200"; - expect(checkBodyExpect(xml, [{ xpath: { path: "/root/status/text()", equals: "ok" } }]).matched).toBe(true); - expect(checkBodyExpect(xml, [{ xpath: { path: "/root/status/text()", equals: "error" } }]).matched).toBe(false); + expect(checkBodyExpect(xml, [{ xpath: { equals: "ok", path: "/root/status/text()" } }]).matched).toBe(true); + expect(checkBodyExpect(xml, [{ xpath: { equals: "error", path: "/root/status/text()" } }]).matched).toBe(false); }); test("xpath 无匹配节点", () => { const xml = "ok"; - const r = checkBodyExpect(xml, [{ xpath: { path: "/root/missing/text()", equals: "ok" } }]); + const r = checkBodyExpect(xml, [{ xpath: { equals: "ok", path: "/root/missing/text()" } }]); expect(r.matched).toBe(false); }); test("规则数组按顺序检查,第一条失败立即返回", () => { const body = JSON.stringify({ status: "error" }); - const r = checkBodyExpect(body, [{ contains: "healthy" }, { json: { path: "$.status", equals: "error" } }]); + const r = checkBodyExpect(body, [{ contains: "healthy" }, { json: { equals: "error", path: "$.status" } }]); expect(r.matched).toBe(false); expect(r.failure!.path).toBe("body[0]"); }); test("多条规则全部通过", () => { - const body = JSON.stringify({ status: "healthy", count: 5 }); + const body = JSON.stringify({ count: 5, status: "healthy" }); const r = checkBodyExpect(body, [ { contains: "healthy" }, - { json: { path: "$.status", equals: "healthy" } }, - { json: { path: "$.count", gte: 1 } }, + { json: { equals: "healthy", path: "$.status" } }, + { json: { gte: 1, path: "$.count" } }, ]); expect(r.matched).toBe(true); expect(r.failure).toBeNull(); @@ -131,7 +132,7 @@ describe("checkBodyExpect (BodyRule[])", () => { test("第二条规则失败返回正确索引", () => { const body = JSON.stringify({ status: "ok" }); - const r = checkBodyExpect(body, [{ contains: "ok" }, { json: { path: "$.status", equals: "error" } }]); + const r = checkBodyExpect(body, [{ contains: "ok" }, { json: { equals: "error", path: "$.status" } }]); expect(r.matched).toBe(false); expect(r.failure!.path).toContain("body[1]"); }); diff --git a/tests/server/checker/runner/shared/duration.test.ts b/tests/server/checker/runner/shared/duration.test.ts index 7b1308e..85b7e04 100644 --- a/tests/server/checker/runner/shared/duration.test.ts +++ b/tests/server/checker/runner/shared/duration.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import { checkDuration } from "../../../../../src/server/checker/runner/shared/duration"; describe("checkDuration", () => { diff --git a/tests/server/checker/runner/shared/failure.test.ts b/tests/server/checker/runner/shared/failure.test.ts index c6c755a..3423a90 100644 --- a/tests/server/checker/runner/shared/failure.test.ts +++ b/tests/server/checker/runner/shared/failure.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { truncateActual, mismatchFailure, errorFailure } from "../../../../../src/server/checker/runner/shared/failure"; + +import { errorFailure, mismatchFailure, truncateActual } from "../../../../../src/server/checker/runner/shared/failure"; describe("truncateActual", () => { test("短字符串不截断", () => { @@ -43,12 +44,12 @@ describe("mismatchFailure", () => { test("返回正确的 mismatch 结构", () => { const f = mismatchFailure("status", "status", [200], 500, "status mismatch"); expect(f).toEqual({ - kind: "mismatch", - phase: "status", - path: "status", - expected: [200], actual: 500, + expected: [200], + kind: "mismatch", message: "status mismatch", + path: "status", + phase: "status", }); }); @@ -65,9 +66,9 @@ describe("errorFailure", () => { const f = errorFailure("body", "body[0].json($.x)", "body is not valid JSON"); expect(f).toEqual({ kind: "error", - phase: "body", - path: "body[0].json($.x)", message: "body is not valid JSON", + path: "body[0].json($.x)", + phase: "body", }); }); diff --git a/tests/server/checker/runner/shared/operator.test.ts b/tests/server/checker/runner/shared/operator.test.ts index 2664589..31e52e6 100644 --- a/tests/server/checker/runner/shared/operator.test.ts +++ b/tests/server/checker/runner/shared/operator.test.ts @@ -1,19 +1,24 @@ import { describe, expect, test } from "bun:test"; -import { applyOperator, checkExpectValue, evaluateJsonPath } from "../../../../../src/server/checker/runner/shared/operator"; + +import { + applyOperator, + checkExpectValue, + evaluateJsonPath, +} from "../../../../../src/server/checker/runner/shared/operator"; describe("evaluateJsonPath", () => { const obj = { - status: "ok", - code: 0, active: true, - error: null, + code: 0, data: { count: 42, items: [{ name: "a" }, { name: "b" }], nested: { deep: "value" }, }, - emptyObj: {}, emptyArr: [], + emptyObj: {}, + error: null, + status: "ok", }; test("简单字段访问", () => { diff --git a/tests/server/checker/runner/shared/text.test.ts b/tests/server/checker/runner/shared/text.test.ts index 97aaee4..0edd624 100644 --- a/tests/server/checker/runner/shared/text.test.ts +++ b/tests/server/checker/runner/shared/text.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import { checkTextRules } from "../../../../../src/server/checker/runner/shared/text"; describe("checkTextRules", () => { @@ -21,7 +22,11 @@ describe("checkTextRules", () => { }); test("多条规则全部通过", () => { - const r = checkTextRules("version: 3.2.1, build: ok", [{ contains: "version" }, { match: "\\d+\\.\\d+\\.\\d+" }], "stdout"); + const r = checkTextRules( + "version: 3.2.1, build: ok", + [{ contains: "version" }, { match: "\\d+\\.\\d+\\.\\d+" }], + "stdout", + ); expect(r.matched).toBe(true); }); diff --git a/tests/server/checker/size.test.ts b/tests/server/checker/size.test.ts index 34a8c9e..ff60f3b 100644 --- a/tests/server/checker/size.test.ts +++ b/tests/server/checker/size.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import { parseSize } from "../../../src/server/checker/size"; describe("parseSize", () => { diff --git a/tests/server/checker/store.test.ts b/tests/server/checker/store.test.ts index 806a31d..0a472eb 100644 --- a/tests/server/checker/store.test.ts +++ b/tests/server/checker/store.test.ts @@ -1,12 +1,14 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test"; -import { ProbeStore } from "../../../src/server/checker/store"; -import type { CheckFailure, ResolvedTarget } from "../../../src/server/checker/types"; import { mkdir, rm } from "node:fs/promises"; -import { join } from "node:path"; import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import type { CheckFailure, ResolvedTarget } from "../../../src/server/checker/types"; + import { checkerRegistry } from "../../../src/server/checker/runner"; -import { HttpChecker } from "../../../src/server/checker/runner/http/runner"; import { CommandChecker } from "../../../src/server/checker/runner/command/runner"; +import { HttpChecker } from "../../../src/server/checker/runner/http/runner"; +import { ProbeStore } from "../../../src/server/checker/store"; function ensureRegistered() { if (!checkerRegistry.supportedTypes.includes("http")) { @@ -20,33 +22,33 @@ beforeAll(() => { }); const httpTarget: ResolvedTarget = { - type: "http", - name: "test-http", + expect: { maxDurationMs: 3000, status: [200] }, group: "default", http: { - url: "https://example.com/health", - method: "GET", headers: { Accept: "application/json" }, maxBodyBytes: 104857600, + method: "GET", + url: "https://example.com/health", }, intervalMs: 30000, + name: "test-http", timeoutMs: 10000, - expect: { status: [200], maxDurationMs: 3000 }, + type: "http", }; const commandTarget: ResolvedTarget = { - type: "command", - name: "test-cmd", - group: "default", command: { - exec: "ping", args: ["-c", "1", "localhost"], cwd: "/tmp", env: {}, + exec: "ping", maxOutputBytes: 104857600, }, + group: "default", intervalMs: 60000, + name: "test-cmd", timeoutMs: 5000, + type: "command", }; describe("ProbeStore", () => { @@ -61,7 +63,7 @@ describe("ProbeStore", () => { afterAll(async () => { store.close(); - await rm(tempDir, { recursive: true, force: true }); + await rm(tempDir, { force: true, recursive: true }); }); test("初始化后无 targets", () => { @@ -80,21 +82,26 @@ describe("ProbeStore", () => { const t = store.getTargets().find((t) => t.name === "test-http")!; expect(t.type).toBe("http"); expect(t.target).toBe("https://example.com/health"); - const config = JSON.parse(t.config); + const config = JSON.parse(t.config) as { + headers: Record; + maxBodyBytes: number; + method: string; + url: string; + }; expect(config.url).toBe("https://example.com/health"); expect(config.method).toBe("GET"); expect(config.headers).toEqual({ Accept: "application/json" }); expect(config.maxBodyBytes).toBe(104857600); expect(t.interval_ms).toBe(30000); expect(t.timeout_ms).toBe(10000); - expect(JSON.parse(t.expect!)).toEqual({ status: [200], maxDurationMs: 3000 }); + expect(JSON.parse(t.expect!)).toEqual({ maxDurationMs: 3000, status: [200] }); }); test("command target 字段正确", () => { const t = store.getTargets().find((t) => t.name === "test-cmd")!; expect(t.type).toBe("command"); expect(t.target).toBe("exec ping -c 1 localhost"); - const config = JSON.parse(t.config); + const config = JSON.parse(t.config) as { args: string[]; cwd: string; exec: string; maxOutputBytes: number }; expect(config.exec).toBe("ping"); expect(config.args).toEqual(["-c", "1", "localhost"]); expect(config.cwd).toBe("/tmp"); @@ -144,39 +151,39 @@ describe("ProbeStore", () => { const t1Id = targets[0]!.id; store.insertCheckResult({ + durationMs: 150.5, + failure: null, + matched: true, + statusDetail: "200 OK", targetId: t1Id, timestamp: "2025-01-01T00:00:00.000Z", - matched: true, - durationMs: 150.5, - statusDetail: "200 OK", - failure: null, }); store.insertCheckResult({ + durationMs: 300, + failure: null, + matched: true, + statusDetail: "200 OK", targetId: t1Id, timestamp: "2025-01-01T00:00:30.000Z", - matched: true, - durationMs: 300, - statusDetail: "200 OK", - failure: null, }); const failure: CheckFailure = { - kind: "error", - phase: "duration", - path: "$.maxDurationMs", - expected: 3000, actual: 5000, + expected: 3000, + kind: "error", message: "请求耗时 5000ms 超过限制 3000ms", + path: "$.maxDurationMs", + phase: "duration", }; store.insertCheckResult({ + durationMs: null, + failure, + matched: false, + statusDetail: null, targetId: t1Id, timestamp: "2025-01-01T00:01:00.000Z", - matched: false, - durationMs: null, - statusDetail: null, - failure, }); const history = store.getHistory(t1Id, "2025-01-01T00:00:00.000Z", "2025-12-31T23:59:59.999Z", 1, 10); @@ -198,12 +205,12 @@ describe("ProbeStore", () => { for (let i = 0; i < 25; i++) { store.insertCheckResult({ + durationMs: 100 + i, + failure: null, + matched: true, + statusDetail: "200 OK", targetId: t1Id, timestamp: `2025-01-01T01:${String(i).padStart(2, "0")}:00.000Z`, - matched: true, - durationMs: 100 + i, - statusDetail: "200 OK", - failure: null, }); } @@ -274,32 +281,32 @@ describe("ProbeStore", () => { test("删除 target 级联删除 check_results", () => { const cascadeStore = new ProbeStore(join(tempDir, "cascade.db")); const cascadeTarget: ResolvedTarget = { - type: "http", - name: "cascade-test", group: "default", - http: { url: "http://cascade.test", method: "GET", headers: {}, maxBodyBytes: 104857600 }, + http: { headers: {}, maxBodyBytes: 104857600, method: "GET", url: "http://cascade.test" }, intervalMs: 30000, + name: "cascade-test", timeoutMs: 10000, + type: "http", }; cascadeStore.syncTargets([cascadeTarget]); const t = cascadeStore.getTargets()[0]!; cascadeStore.insertCheckResult({ + durationMs: 100, + failure: null, + matched: true, + statusDetail: "200 OK", targetId: t.id, timestamp: "2025-01-01T00:00:00.000Z", - matched: true, - durationMs: 100, - statusDetail: "200 OK", - failure: null, }); cascadeStore.insertCheckResult({ + durationMs: null, + failure: { kind: "error", message: "fail", path: "$", phase: "status" }, + matched: false, + statusDetail: null, targetId: t.id, timestamp: "2025-01-01T00:01:00.000Z", - matched: false, - durationMs: null, - statusDetail: null, - failure: { kind: "error", phase: "status", path: "$", message: "fail" }, }); expect(cascadeStore.getLatestCheck(t.id)).not.toBeNull(); @@ -329,12 +336,12 @@ describe("ProbeStore", () => { const freshStore = new ProbeStore(join(tempDir, "fresh-map.db")); freshStore.syncTargets([ { - type: "http", - name: "no-records", group: "default", - http: { url: "http://no.records", method: "GET", headers: {}, maxBodyBytes: 104857600 }, + http: { headers: {}, maxBodyBytes: 104857600, method: "GET", url: "http://no.records" }, intervalMs: 30000, + name: "no-records", timeoutMs: 10000, + type: "http", }, ]); @@ -359,8 +366,8 @@ describe("ProbeStore", () => { const stats2 = stats.get(t2Id); if (stats2) { - expect(stats2!.totalChecks).toBe(0); - expect(stats2!.availability).toBe(0); + expect(stats2.totalChecks).toBe(0); + expect(stats2.availability).toBe(0); } }); @@ -368,12 +375,12 @@ describe("ProbeStore", () => { const freshStore = new ProbeStore(join(tempDir, "fresh-stats.db")); freshStore.syncTargets([ { - type: "http", - name: "no-stats", group: "default", - http: { url: "http://no.stats", method: "GET", headers: {}, maxBodyBytes: 104857600 }, + http: { headers: {}, maxBodyBytes: 104857600, method: "GET", url: "http://no.stats" }, intervalMs: 30000, + name: "no-stats", timeoutMs: 10000, + type: "http", }, ]); diff --git a/tests/server/config.test.ts b/tests/server/config.test.ts index db305fc..bd20e5e 100644 --- a/tests/server/config.test.ts +++ b/tests/server/config.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; + import { readRuntimeConfig } from "../../src/server/config"; describe("runtime config", () => { diff --git a/tests/web/constants/color-threshold.test.ts b/tests/web/constants/color-threshold.test.ts index e4d3d37..cb1f7fb 100644 --- a/tests/web/constants/color-threshold.test.ts +++ b/tests/web/constants/color-threshold.test.ts @@ -1,4 +1,5 @@ -import { describe, test, expect } from "bun:test"; +import { describe, expect, test } from "bun:test"; + import { getAvailabilityProgressColor } from "../../../src/web/constants/color-threshold"; describe("color-threshold", () => { diff --git a/tests/web/constants/target-table-filters.test.ts b/tests/web/constants/target-table-filters.test.ts index fa2b5e2..3d39daf 100644 --- a/tests/web/constants/target-table-filters.test.ts +++ b/tests/web/constants/target-table-filters.test.ts @@ -1,4 +1,5 @@ -import { describe, test, expect } from "bun:test"; +import { describe, expect, test } from "bun:test"; + import { statusFilter, typeFilter } from "../../../src/web/constants/target-table-filters"; describe("target-table-filters", () => { diff --git a/tests/web/constants/target-table-sorters.test.ts b/tests/web/constants/target-table-sorters.test.ts index 3452aea..13c98c3 100644 --- a/tests/web/constants/target-table-sorters.test.ts +++ b/tests/web/constants/target-table-sorters.test.ts @@ -1,23 +1,25 @@ -import { describe, test, expect } from "bun:test"; +import { describe, expect, test } from "bun:test"; + +import type { TargetStatus } from "../../../src/shared/api"; + import { - statusSorter, availabilitySorter, latencySorter, nameSorter, + statusSorter, } from "../../../src/web/constants/target-table-sorters"; -import type { TargetStatus } from "../../../src/shared/api"; function makeTarget(overrides: Partial = {}): TargetStatus { return { - id: 1, - name: "test", - type: "http", - target: "https://example.com", group: "default", + id: 1, interval: "5s", latestCheck: null, - stats: { totalChecks: 0, availability: 100 }, + name: "test", recentSamples: [], + stats: { availability: 100, totalChecks: 0 }, + target: "https://example.com", + type: "http", ...overrides, }; } @@ -25,10 +27,10 @@ function makeTarget(overrides: Partial = {}): TargetStatus { describe("statusSorter", () => { test("DOWN 排在 UP 前面", () => { const up = makeTarget({ - latestCheck: { timestamp: "", matched: true, durationMs: 10, statusDetail: null, failure: null }, + latestCheck: { durationMs: 10, failure: null, matched: true, statusDetail: null, timestamp: "" }, }); const down = makeTarget({ - latestCheck: { timestamp: "", matched: false, durationMs: 10, statusDetail: null, failure: null }, + latestCheck: { durationMs: 10, failure: null, matched: false, statusDetail: null, timestamp: "" }, }); expect(statusSorter(down, up)).toBeLessThan(0); expect(statusSorter(up, down)).toBeGreaterThan(0); @@ -36,10 +38,10 @@ describe("statusSorter", () => { test("相同状态返回 0", () => { const a = makeTarget({ - latestCheck: { timestamp: "", matched: true, durationMs: 10, statusDetail: null, failure: null }, + latestCheck: { durationMs: 10, failure: null, matched: true, statusDetail: null, timestamp: "" }, }); const b = makeTarget({ - latestCheck: { timestamp: "", matched: true, durationMs: 20, statusDetail: null, failure: null }, + latestCheck: { durationMs: 20, failure: null, matched: true, statusDetail: null, timestamp: "" }, }); expect(statusSorter(a, b)).toBe(0); }); @@ -47,7 +49,7 @@ describe("statusSorter", () => { test("无 latestCheck 的目标排在最后", () => { const noCheck = makeTarget(); const up = makeTarget({ - latestCheck: { timestamp: "", matched: true, durationMs: 10, statusDetail: null, failure: null }, + latestCheck: { durationMs: 10, failure: null, matched: true, statusDetail: null, timestamp: "" }, }); expect(statusSorter(noCheck, up)).toBeGreaterThan(0); }); @@ -55,20 +57,20 @@ describe("statusSorter", () => { describe("availabilitySorter", () => { test("低可用率排前面", () => { - const low = makeTarget({ stats: { totalChecks: 100, availability: 95 } }); - const high = makeTarget({ stats: { totalChecks: 100, availability: 99.9 } }); + const low = makeTarget({ stats: { availability: 95, totalChecks: 100 } }); + const high = makeTarget({ stats: { availability: 99.9, totalChecks: 100 } }); expect(availabilitySorter(low, high)).toBeLessThan(0); }); test("相同可用率返回 0", () => { - const a = makeTarget({ stats: { totalChecks: 100, availability: 99.9 } }); - const b = makeTarget({ stats: { totalChecks: 50, availability: 99.9 } }); + const a = makeTarget({ stats: { availability: 99.9, totalChecks: 100 } }); + const b = makeTarget({ stats: { availability: 99.9, totalChecks: 50 } }); expect(availabilitySorter(a, b)).toBe(0); }); test("无 stats 按 0 处理", () => { const noStats = makeTarget({ stats: undefined as unknown as TargetStatus["stats"] }); - const high = makeTarget({ stats: { totalChecks: 100, availability: 99.9 } }); + const high = makeTarget({ stats: { availability: 99.9, totalChecks: 100 } }); expect(availabilitySorter(noStats, high)).toBeLessThan(0); }); }); @@ -76,20 +78,20 @@ describe("availabilitySorter", () => { describe("latencySorter", () => { test("低延迟排前面", () => { const fast = makeTarget({ - latestCheck: { timestamp: "", matched: true, durationMs: 50, statusDetail: null, failure: null }, + latestCheck: { durationMs: 50, failure: null, matched: true, statusDetail: null, timestamp: "" }, }); const slow = makeTarget({ - latestCheck: { timestamp: "", matched: true, durationMs: 200, statusDetail: null, failure: null }, + latestCheck: { durationMs: 200, failure: null, matched: true, statusDetail: null, timestamp: "" }, }); expect(latencySorter(fast, slow)).toBeLessThan(0); }); test("无延迟排最后", () => { const noLatency = makeTarget({ - latestCheck: { timestamp: "", matched: true, durationMs: null, statusDetail: null, failure: null }, + latestCheck: { durationMs: null, failure: null, matched: true, statusDetail: null, timestamp: "" }, }); const hasLatency = makeTarget({ - latestCheck: { timestamp: "", matched: true, durationMs: 100, statusDetail: null, failure: null }, + latestCheck: { durationMs: 100, failure: null, matched: true, statusDetail: null, timestamp: "" }, }); expect(latencySorter(noLatency, hasLatency)).toBeGreaterThan(0); }); diff --git a/tests/web/constants/target-type-display.test.ts b/tests/web/constants/target-type-display.test.ts index cde1579..752f309 100644 --- a/tests/web/constants/target-type-display.test.ts +++ b/tests/web/constants/target-type-display.test.ts @@ -1,5 +1,6 @@ -import { describe, test, expect } from "bun:test"; -import { TARGET_TYPE_DISPLAY, getTargetTypeDisplay } from "../../../src/web/constants/target-type-display"; +import { describe, expect, test } from "bun:test"; + +import { getTargetTypeDisplay, TARGET_TYPE_DISPLAY } from "../../../src/web/constants/target-type-display"; describe("target-type-display", () => { describe("TARGET_TYPE_DISPLAY 常量", () => { diff --git a/tsconfig.json b/tsconfig.json index 612e431..7ee70d8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,8 +23,8 @@ "noImplicitOverride": true, // Some stricter flags (disabled by default) - "noUnusedLocals": false, + "noUnusedLocals": true, "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false + "noPropertyAccessFromIndexSignature": true } } diff --git a/vite.config.ts b/vite.config.ts index 500cc9b..5ff9edb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,25 +1,25 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; -const backendPort = Number(process.env.BACKEND_PORT ?? process.env.PORT ?? 3000); +const backendPort = Number(process.env["BACKEND_PORT"] ?? process.env["PORT"] ?? 3000); export default defineConfig({ - root: "src/web", + build: { + assetsDir: "assets", + emptyOutDir: true, + outDir: "../../dist/web", + }, plugins: [react()], + root: "src/web", server: { host: "127.0.0.1", port: 5173, - strictPort: true, proxy: { "/api": { - target: `http://127.0.0.1:${backendPort}`, changeOrigin: true, + target: `http://127.0.0.1:${backendPort}`, }, }, - }, - build: { - outDir: "../../dist/web", - emptyOutDir: true, - assetsDir: "assets", + strictPort: true, }, });