From 52007c94611628a3646fab4773d0562dc54ea932 Mon Sep 17 00:00:00 2001 From: lanyuanxiaoyao Date: Thu, 23 Apr 2026 22:47:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=89=8D=E7=AB=AF=20ESLint=20=E8=A7=84?= =?UTF-8?q?=E5=88=99=E5=A2=9E=E5=BC=BA=EF=BC=8C=E8=87=AA=E5=8A=A8=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=20LLM=20=E7=BC=96=E7=A0=81=E8=BF=9D=E8=A7=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 启用 TanStack Query flat/recommended(7 条规则) - 新增 no-console(允许 warn/error)、consistent-type-imports(inline 风格)、no-non-null-assertion 规则 - 新增自定义规则 no-hardcoded-color-in-style,检测 JSX style 中硬编码颜色值 - 将 ESLint 检查集成到 build 命令(tsc -b && eslint . && vite build) - 修复现有代码中的 lint 违规(import 顺序、type import 风格、unused vars) - 使用 @typescript-eslint/rule-tester 编写自定义规则集成测试 --- frontend/bun.lock | 105 +++++++++++++-- frontend/eslint-rules/index.js | 13 ++ .../rules/no-hardcoded-color-in-style.js | 112 ++++++++++++++++ frontend/eslint.config.js | 19 +++ frontend/package.json | 4 +- frontend/playwright.config.ts | 2 +- frontend/src/__tests__/api/client.test.ts | 4 +- frontend/src/__tests__/api/models.test.ts | 4 +- frontend/src/__tests__/api/providers.test.ts | 8 +- frontend/src/__tests__/api/stats.test.ts | 4 +- .../__tests__/components/AppLayout.test.tsx | 2 +- .../eslint-rules/no-hardcoded-color.test.ts | 124 ++++++++++++++++++ .../src/__tests__/hooks/useModels.test.tsx | 6 +- .../src/__tests__/hooks/useProviders.test.tsx | 6 +- .../src/__tests__/hooks/useStats.test.tsx | 4 +- frontend/src/components/AppLayout/index.tsx | 4 +- frontend/src/hooks/useModels.ts | 2 +- frontend/src/hooks/useProviders.ts | 2 +- frontend/src/hooks/useStats.ts | 2 +- frontend/src/main.tsx | 6 +- frontend/src/pages/NotFound.tsx | 2 +- frontend/src/pages/Providers/ModelTable.tsx | 4 +- .../src/pages/Providers/ProviderTable.tsx | 2 +- frontend/src/pages/Providers/index.tsx | 8 +- frontend/src/pages/Stats/StatCards.tsx | 2 +- frontend/src/pages/Stats/StatsTable.tsx | 2 +- frontend/src/pages/Stats/UsageChart.tsx | 2 +- frontend/src/pages/Stats/index.tsx | 2 +- frontend/vite.config.ts | 6 +- frontend/vitest.config.ts | 4 +- openspec/specs/frontend-lint-rules/spec.md | 114 ++++++++++++++++ openspec/specs/frontend/spec.md | 5 + 32 files changed, 531 insertions(+), 55 deletions(-) create mode 100644 frontend/eslint-rules/index.js create mode 100644 frontend/eslint-rules/rules/no-hardcoded-color-in-style.js create mode 100644 frontend/src/__tests__/eslint-rules/no-hardcoded-color.test.ts create mode 100644 openspec/specs/frontend-lint-rules/spec.md diff --git a/frontend/bun.lock b/frontend/bun.lock index 5a2c2c2..183cbf3 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -25,6 +25,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/sql.js": "^1.4.11", + "@typescript-eslint/rule-tester": "^8.59.0", "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^3.2.1", "eslint": "^9.39.4", @@ -425,23 +426,25 @@ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.58.2.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.0.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.0", "@typescript-eslint/types": "^8.59.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw=="], + + "@typescript-eslint/rule-tester": ["@typescript-eslint/rule-tester@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/rule-tester/-/rule-tester-8.59.0.tgz", { "dependencies": { "@typescript-eslint/parser": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0", "@typescript-eslint/utils": "8.59.0", "ajv": "^6.12.6", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "4.6.2", "semver": "^7.7.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "sha512-2Ej6W28DqObFuEUQ+puEpDZFWFXAW7jIZ4TsgfLUCTNz1FID0NMfp1sXc+fQq8m5ysfPdhXAPjti6jYEu1oRcg=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.58.2.tgz", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.59.0", "@typescript-eslint/tsconfig-utils": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.58.2.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q=="], "@vercel/blob": ["@vercel/blob@2.3.3", "https://registry.npmmirror.com/@vercel/blob/-/blob-2.3.3.tgz", { "dependencies": { "async-retry": "^1.3.3", "is-buffer": "^2.0.5", "is-node-process": "^1.2.0", "throttleit": "^2.1.0", "undici": "^6.23.0" } }, "sha512-MtD7VLo6hU07eHR7bmk5SIMD290q574UaNYTe46qeyRT+hWrCy26CoAqfd7PnIefVXvRehRZBzukxuTO9iGTVg=="], @@ -1153,7 +1156,7 @@ "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=="], + "semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], @@ -1379,6 +1382,10 @@ "@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=="], + "@babel/core/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=="], + + "@babel/helper-compilation-targets/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=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -1397,11 +1404,29 @@ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0" } }, "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg=="], + + "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.0.tgz", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], + + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.0.tgz", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], + + "@typescript-eslint/rule-tester/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.0.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g=="], + + "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], + + "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.0.tgz", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], + "@typescript-eslint/typescript-estree/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=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], + + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.0.tgz", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], @@ -1415,8 +1440,6 @@ "conf/env-paths": ["env-paths@3.0.0", "https://registry.npmmirror.com/env-paths/-/env-paths-3.0.0.tgz", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], - "conf/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "data-urls/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], "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=="], @@ -1425,6 +1448,8 @@ "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/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=="], + "espree/acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "glob/minimatch": ["minimatch@9.0.9", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], @@ -1435,12 +1460,12 @@ "loose-envify/js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "make-dir/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "md5/is-buffer": ["is-buffer@1.1.6", "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], "msw/tough-cookie": ["tough-cookie@6.0.1", "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="], + "node-exports-info/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=="], + "parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "path-scurry/lru-cache": ["lru-cache@10.4.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -1463,6 +1488,10 @@ "test-exclude/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=="], + "typescript-eslint/@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.58.2.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="], + + "typescript-eslint/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], + "vite-node/vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], "vite-plugin-javascript-obfuscator/javascript-obfuscator": ["javascript-obfuscator@4.2.2", "https://registry.npmmirror.com/javascript-obfuscator/-/javascript-obfuscator-4.2.2.tgz", { "dependencies": { "@javascript-obfuscator/escodegen": "2.3.1", "@javascript-obfuscator/estraverse": "5.4.0", "acorn": "8.15.0", "assert": "2.1.0", "chalk": "4.1.2", "chance": "1.1.13", "class-validator": "0.14.3", "commander": "12.1.0", "conf": "15.0.2", "eslint-scope": "8.4.0", "eslint-visitor-keys": "4.2.1", "fast-deep-equal": "3.1.3", "inversify": "6.1.4", "js-string-escape": "1.0.1", "md5": "2.3.0", "mkdirp": "3.0.1", "multimatch": "5.0.0", "process": "0.11.10", "reflect-metadata": "0.2.2", "source-map-support": "0.5.21", "string-template": "1.0.0", "stringz": "2.1.0", "tslib": "2.8.1" }, "bin": { "javascript-obfuscator": "bin/javascript-obfuscator" } }, "sha512-+7oXAUnFCA6vS0omIGHcWpSr67dUBIF7FKGYSXyzxShSLqM6LBgdugWKFl0XrYtGWyJMGfQR5F4LL85iCefkRA=="], @@ -1481,8 +1510,32 @@ "@javascript-obfuscator/escodegen/optionator/type-check": ["type-check@0.3.2", "https://registry.npmmirror.com/type-check/-/type-check-0.3.2.tgz", { "dependencies": { "prelude-ls": "~1.1.2" } }, "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "@typescript-eslint/rule-tester/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.0", "@typescript-eslint/visitor-keys": "8.59.0" } }, "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg=="], + + "@typescript-eslint/rule-tester/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.0", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.0.tgz", {}, "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A=="], + + "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/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=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/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=="], + "ajv-formats/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=="], "conf/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=="], @@ -1493,16 +1546,46 @@ "test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/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=="], + "vite-plugin-javascript-obfuscator/javascript-obfuscator/@javascript-obfuscator/escodegen": ["@javascript-obfuscator/escodegen@2.3.1", "https://registry.npmmirror.com/@javascript-obfuscator/escodegen/-/escodegen-2.3.1.tgz", { "dependencies": { "@javascript-obfuscator/estraverse": "^5.3.0", "esprima": "^4.0.1", "esutils": "^2.0.2", "optionator": "^0.8.1" }, "optionalDependencies": { "source-map": "~0.6.1" } }, "sha512-Z0HEAVwwafOume+6LFXirAVZeuEMKWuPzpFbQhCEU9++BMz0IwEa9bmedJ+rMn/IlXRBID9j3gQ0XYAa6jM10g=="], + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "msw/tough-cookie/tldts/tldts-core": ["tldts-core@7.0.28", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.28.tgz", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="], "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "typescript-eslint/@typescript-eslint/parser/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "vite-plugin-javascript-obfuscator/javascript-obfuscator/@javascript-obfuscator/escodegen/optionator": ["optionator@0.8.3", "https://registry.npmmirror.com/optionator/-/optionator-0.8.3.tgz", { "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", "word-wrap": "~1.2.3" } }, "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA=="], + "@typescript-eslint/type-utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "typescript-eslint/@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "vite-plugin-javascript-obfuscator/javascript-obfuscator/@javascript-obfuscator/escodegen/optionator/levn": ["levn@0.3.0", "https://registry.npmmirror.com/levn/-/levn-0.3.0.tgz", { "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" } }, "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA=="], "vite-plugin-javascript-obfuscator/javascript-obfuscator/@javascript-obfuscator/escodegen/optionator/prelude-ls": ["prelude-ls@1.1.2", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.1.2.tgz", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="], diff --git a/frontend/eslint-rules/index.js b/frontend/eslint-rules/index.js new file mode 100644 index 0000000..317b657 --- /dev/null +++ b/frontend/eslint-rules/index.js @@ -0,0 +1,13 @@ +import noHardcodedColorInStyle from './rules/no-hardcoded-color-in-style.js' + +const plugin = { + rules: { + 'no-hardcoded-color-in-style': noHardcodedColorInStyle, + }, + configs: {}, + meta: { + name: 'eslint-plugin-local', + }, +} + +export default plugin \ No newline at end of file diff --git a/frontend/eslint-rules/rules/no-hardcoded-color-in-style.js b/frontend/eslint-rules/rules/no-hardcoded-color-in-style.js new file mode 100644 index 0000000..f52e96c --- /dev/null +++ b/frontend/eslint-rules/rules/no-hardcoded-color-in-style.js @@ -0,0 +1,112 @@ +import { ESLintUtils } from '@typescript-eslint/utils' + +const RE_HEX3 = /^#[0-9a-fA-F]{3}$/ +const RE_HEX6 = /^#[0-9a-fA-F]{6}$/ +const RE_HEX8 = /^#[0-9a-fA-F]{8}$/ +const RE_RGB = /^rgb\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/ +const RE_RGBA = /^rgba\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)$/ +const RE_HSL = /^hsl\s*\(\s*\d+\s*,\s*[\d.]+%?\s*,\s*[\d.]+%?\s*\)$/ + +const ALLOWED_KEYWORDS = new Set([ + 'inherit', + 'transparent', + 'currentColor', + 'none', + 'unset', + 'initial', + 'auto', + 'contain', + 'cover', +]) + +function isHardcodedColor(value) { + if (typeof value !== 'string') return false + + const trimmed = value.trim() + + if (ALLOWED_KEYWORDS.has(trimmed.toLowerCase())) return false + if (trimmed.startsWith('var(')) return false + if (/^\d+(\.\d+)?px?$/.test(trimmed)) return false + if (/^\d+(\.\d+)?\%$/.test(trimmed)) return false + + return ( + RE_HEX3.test(trimmed) || + RE_HEX6.test(trimmed) || + RE_HEX8.test(trimmed) || + RE_RGB.test(trimmed) || + RE_RGBA.test(trimmed) || + RE_HSL.test(trimmed) + ) +} + +function extractStyleProperties(expression) { + const properties = [] + + if ( + expression.type === 'ObjectExpression' && + expression.properties + ) { + for (const styleProp of expression.properties) { + if ( + styleProp.type === 'Property' && + styleProp.key?.type === 'Identifier' && + styleProp.value?.type === 'Literal' && + typeof styleProp.value.value === 'string' + ) { + properties.push({ + key: styleProp.key.name, + value: styleProp.value.value, + loc: styleProp.value.loc, + }) + } + } + } + + return properties +} + +export const RULE_NAME = 'no-hardcoded-color-in-style' + +export default ESLintUtils.RuleCreator((name) => { + return `https://eslint.dev/rules/#${name}` +})({ + name: 'no-hardcoded-color-in-style', + meta: { + type: 'problem', + docs: { + description: 'Disallow hardcoded color values in JSX style properties', + recommended: false, + }, + messages: { + hardcodedColor: + '硬编码的颜色值 "{{value}}" 不允许使用。请使用 TDesign CSS Token(如 var(--td-text-color-placeholder))代替。', + }, + schema: [], + }, + create(context) { + return { + JSXAttribute(node) { + if ( + node.name?.type === 'JSXIdentifier' && + node.name.name === 'style' && + node.value?.type === 'JSXExpressionContainer' && + node.value.expression + ) { + const styleProps = extractStyleProperties( + node.value.expression, + ) + + for (const prop of styleProps) { + if (isHardcodedColor(prop.value)) { + context.report({ + node: context.sourceCode.getLastToken(node), + messageId: 'hardcodedColor', + data: { value: prop.value }, + }) + } + } + } + }, + } + }, +}) \ No newline at end of file diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 32e3be5..f0bf189 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -5,9 +5,11 @@ import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' import importPlugin from 'eslint-plugin-import' import tanstackQuery from '@tanstack/eslint-plugin-query' +import localRules from './eslint-rules/index.js' export default tseslint.config( { ignores: ['dist'] }, + ...tanstackQuery.configs['flat/recommended'], { extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], @@ -20,6 +22,7 @@ export default tseslint.config( 'react-refresh': reactRefresh, import: importPlugin, '@tanstack/query': tanstackQuery, + local: localRules, }, rules: { ...reactHooks.configs.recommended.rules, @@ -27,6 +30,13 @@ export default tseslint.config( 'warn', { allowConstantExport: true }, ], + 'no-console': ['error', { allow: ['warn', 'error'] }], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', fixStyle: 'inline-type-imports' }, + ], + '@typescript-eslint/no-non-null-assertion': 'error', + 'local/no-hardcoded-color-in-style': 'warn', 'import/order': [ 'warn', { @@ -48,4 +58,13 @@ export default tseslint.config( ], }, }, + { + files: ['src/__tests__/**', 'e2e/**'], + rules: { + '@typescript-eslint/no-non-null-assertion': 'off', + 'react-hooks/exhaustive-deps': 'off', + '@typescript-eslint/consistent-type-imports': 'off', + 'no-console': 'off', + }, + }, ) diff --git a/frontend/package.json b/frontend/package.json index f090dd9..672d2fd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,8 +5,9 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", + "build": "tsc -b && eslint . && vite build", "lint": "eslint .", + "lint:fix": "eslint . --fix", "preview": "vite preview", "test": "vitest run", "test:watch": "vitest", @@ -34,6 +35,7 @@ "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@types/sql.js": "^1.4.11", + "@typescript-eslint/rule-tester": "^8.59.0", "@vitejs/plugin-react": "^6.0.1", "@vitest/coverage-v8": "^3.2.1", "eslint": "^9.39.4", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 118cf03..44d7677 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -1,8 +1,8 @@ -import { defineConfig, devices } from '@playwright/test' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { defineConfig, devices } from '@playwright/test' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) diff --git a/frontend/src/__tests__/api/client.test.ts b/frontend/src/__tests__/api/client.test.ts index 0fb33a2..47c29bd 100644 --- a/frontend/src/__tests__/api/client.test.ts +++ b/frontend/src/__tests__/api/client.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; -import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; import { request, fromApi, toApi } from '@/api/client'; import { ApiError } from '@/types'; diff --git a/frontend/src/__tests__/api/models.test.ts b/frontend/src/__tests__/api/models.test.ts index 518d45a..264a47c 100644 --- a/frontend/src/__tests__/api/models.test.ts +++ b/frontend/src/__tests__/api/models.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; -import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; import { listModels, createModel, updateModel, deleteModel } from '@/api/models'; const mockModels = [ diff --git a/frontend/src/__tests__/api/providers.test.ts b/frontend/src/__tests__/api/providers.test.ts index 7af422e..b75bde5 100644 --- a/frontend/src/__tests__/api/providers.test.ts +++ b/frontend/src/__tests__/api/providers.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; -import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; import { listProviders, createProvider, updateProvider, deleteProvider } from '@/api/providers'; const mockProviders = [ @@ -119,7 +119,7 @@ describe('providers API', () => { let receivedBody: Record | null = null; server.use( - http.put('http://localhost:3000/api/providers/:id', async ({ request, params }) => { + http.put('http://localhost:3000/api/providers/:id', async ({ request }) => { receivedMethod = request.method; receivedUrl = new URL(request.url).pathname; receivedBody = (await request.json()) as Record; @@ -153,7 +153,7 @@ describe('providers API', () => { let receivedUrl: string | null = null; server.use( - http.delete('http://localhost:3000/api/providers/:id', ({ request, params }) => { + http.delete('http://localhost:3000/api/providers/:id', ({ request }) => { receivedMethod = request.method; receivedUrl = new URL(request.url).pathname; return new HttpResponse(null, { status: 204 }); diff --git a/frontend/src/__tests__/api/stats.test.ts b/frontend/src/__tests__/api/stats.test.ts index dc00c29..32dd943 100644 --- a/frontend/src/__tests__/api/stats.test.ts +++ b/frontend/src/__tests__/api/stats.test.ts @@ -1,6 +1,6 @@ -import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; -import { setupServer } from 'msw/node'; import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; import { getStats } from '@/api/stats'; const mockStats = [ diff --git a/frontend/src/__tests__/components/AppLayout.test.tsx b/frontend/src/__tests__/components/AppLayout.test.tsx index 414c990..c5ac659 100644 --- a/frontend/src/__tests__/components/AppLayout.test.tsx +++ b/frontend/src/__tests__/components/AppLayout.test.tsx @@ -1,6 +1,6 @@ import { render, screen } from '@testing-library/react'; -import { describe, it, expect } from 'vitest'; import { BrowserRouter } from 'react-router'; +import { describe, it, expect } from 'vitest'; import { AppLayout } from '@/components/AppLayout'; const renderWithRouter = (component: React.ReactNode) => { diff --git a/frontend/src/__tests__/eslint-rules/no-hardcoded-color.test.ts b/frontend/src/__tests__/eslint-rules/no-hardcoded-color.test.ts new file mode 100644 index 0000000..7a467aa --- /dev/null +++ b/frontend/src/__tests__/eslint-rules/no-hardcoded-color.test.ts @@ -0,0 +1,124 @@ +import { RuleTester } from '@typescript-eslint/rule-tester' +import { describe, it, afterAll } from 'vitest' +import rule, { + RULE_NAME, +} from '../../../eslint-rules/rules/no-hardcoded-color-in-style.js' + +RuleTester.it = it +RuleTester.describe = describe +RuleTester.afterAll = afterAll + +const ruleTester = new RuleTester({ + languageOptions: { + parserOptions: { + ecmaFeatures: { jsx: true }, + ecmaVersion: 2023, + sourceType: 'module', + }, + }, +}) + +describe('no-hardcoded-color-in-style (ESLint rule)', () => { + ruleTester.run(RULE_NAME, rule, { + valid: [ + { + name: 'CSS var token', + code: `
`, + }, + { + name: 'numeric value 0', + code: `
`, + }, + { + name: 'numeric value 16', + code: `
`, + }, + { + name: 'inherit keyword', + code: `
`, + }, + { + name: 'transparent keyword', + code: `
`, + }, + { + name: 'currentColor keyword', + code: `
`, + }, + { + name: 'none keyword', + code: `
`, + }, + { + name: 'unset keyword', + code: `
`, + }, + { + name: 'initial keyword', + code: `
`, + }, + { + name: 'pixel string value', + code: `
`, + }, + { + name: 'percentage string value', + code: `
`, + }, + { + name: 'plain numeric string value', + code: `
`, + }, + { + name: 'auto keyword', + code: `
`, + }, + { + name: 'contain keyword', + code: `
`, + }, + { + name: 'cover keyword', + code: `
`, + }, + ], + + invalid: [ + { + name: 'hex3 color #fff', + code: `
`, + errors: [{ messageId: 'hardcodedColor', data: { value: '#fff' } }], + }, + { + name: 'hex6 color #ffffff', + code: `
`, + errors: [{ messageId: 'hardcodedColor', data: { value: '#ffffff' } }], + }, + { + name: 'hex8 color #ffffffff', + code: `
`, + errors: [{ messageId: 'hardcodedColor', data: { value: '#ffffffff' } }], + }, + { + name: 'rgb color', + code: `
`, + errors: [{ messageId: 'hardcodedColor', data: { value: 'rgb(255, 255, 255)' } }], + }, + { + name: 'rgba color', + code: `
`, + errors: [{ messageId: 'hardcodedColor', data: { value: 'rgba(255, 255, 255, 0.5)' } }], + }, + { + name: 'hsl color', + code: `
`, + errors: [{ messageId: 'hardcodedColor', data: { value: 'hsl(120, 50%, 50%)' } }], + }, + { + name: 'multiple style properties with one hardcoded', + code: `
`, + errors: [{ messageId: 'hardcodedColor', data: { value: '#999' } }], + }, + ], + }) +}) \ No newline at end of file diff --git a/frontend/src/__tests__/hooks/useModels.test.tsx b/frontend/src/__tests__/hooks/useModels.test.tsx index 47a0eec..2b15088 100644 --- a/frontend/src/__tests__/hooks/useModels.test.tsx +++ b/frontend/src/__tests__/hooks/useModels.test.tsx @@ -1,11 +1,11 @@ -import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import React from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; +import React from 'react'; +import { MessagePlugin } from 'tdesign-react'; import { useModels, useCreateModel, useUpdateModel, useDeleteModel } from '@/hooks/useModels'; import type { Model, CreateModelInput, UpdateModelInput } from '@/types'; -import { MessagePlugin } from 'tdesign-react'; // Mock MessagePlugin vi.mock('tdesign-react', () => ({ diff --git a/frontend/src/__tests__/hooks/useProviders.test.tsx b/frontend/src/__tests__/hooks/useProviders.test.tsx index 1d0436a..e2d289c 100644 --- a/frontend/src/__tests__/hooks/useProviders.test.tsx +++ b/frontend/src/__tests__/hooks/useProviders.test.tsx @@ -1,11 +1,11 @@ -import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import React from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; +import React from 'react'; +import { MessagePlugin } from 'tdesign-react'; import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders'; import type { Provider, CreateProviderInput, UpdateProviderInput } from '@/types'; -import { MessagePlugin } from 'tdesign-react'; // Mock MessagePlugin vi.mock('tdesign-react', () => ({ diff --git a/frontend/src/__tests__/hooks/useStats.test.tsx b/frontend/src/__tests__/hooks/useStats.test.tsx index daa97bb..e4bf99e 100644 --- a/frontend/src/__tests__/hooks/useStats.test.tsx +++ b/frontend/src/__tests__/hooks/useStats.test.tsx @@ -1,8 +1,8 @@ -import { renderHook, waitFor } from '@testing-library/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import React from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; +import React from 'react'; import { useStats } from '@/hooks/useStats'; import type { UsageStats, StatsQueryParams } from '@/types'; diff --git a/frontend/src/components/AppLayout/index.tsx b/frontend/src/components/AppLayout/index.tsx index 88d69c2..e6429c0 100644 --- a/frontend/src/components/AppLayout/index.tsx +++ b/frontend/src/components/AppLayout/index.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; -import { Layout, Menu, Button } from 'tdesign-react'; -import { ServerIcon, ChartLineIcon, SettingIcon, ChevronLeftIcon, ChevronRightIcon } from 'tdesign-icons-react'; import { Outlet, useLocation, useNavigate } from 'react-router'; +import { ServerIcon, ChartLineIcon, SettingIcon, ChevronLeftIcon, ChevronRightIcon } from 'tdesign-icons-react'; +import { Layout, Menu, Button } from 'tdesign-react'; const { MenuItem } = Menu; diff --git a/frontend/src/hooks/useModels.ts b/frontend/src/hooks/useModels.ts index dbf7e58..356c984 100644 --- a/frontend/src/hooks/useModels.ts +++ b/frontend/src/hooks/useModels.ts @@ -1,7 +1,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { MessagePlugin } from 'tdesign-react'; -import type { CreateModelInput, UpdateModelInput, ApiError } from '@/types'; import * as api from '@/api/models'; +import type { CreateModelInput, UpdateModelInput, ApiError } from '@/types'; const ERROR_MESSAGES: Record = { duplicate_model: '同一供应商下模型名称已存在', diff --git a/frontend/src/hooks/useProviders.ts b/frontend/src/hooks/useProviders.ts index fb9836b..1e2e989 100644 --- a/frontend/src/hooks/useProviders.ts +++ b/frontend/src/hooks/useProviders.ts @@ -1,7 +1,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { MessagePlugin } from 'tdesign-react'; -import type { CreateProviderInput, UpdateProviderInput, ApiError } from '@/types'; import * as api from '@/api/providers'; +import type { CreateProviderInput, UpdateProviderInput, ApiError } from '@/types'; const ERROR_MESSAGES: Record = { duplicate_model: '同一供应商下模型名称已存在', diff --git a/frontend/src/hooks/useStats.ts b/frontend/src/hooks/useStats.ts index 8d0867e..c673667 100644 --- a/frontend/src/hooks/useStats.ts +++ b/frontend/src/hooks/useStats.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; -import type { StatsQueryParams } from '@/types'; import * as api from '@/api/stats'; +import type { StatsQueryParams } from '@/types'; export const statsKeys = { filtered: (params?: StatsQueryParams) => ['stats', params] as const, diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index dc9aed9..3dc8d1f 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,7 +5,11 @@ import 'tdesign-react/es/_util/react-19-adapter' import './index.scss' import App from './App' -createRoot(document.getElementById('root')!).render( +const root = document.getElementById('root') +if (!root) { + throw new Error('Root element not found') +} +createRoot(root).render( , diff --git a/frontend/src/pages/NotFound.tsx b/frontend/src/pages/NotFound.tsx index d8ee42c..4d91070 100644 --- a/frontend/src/pages/NotFound.tsx +++ b/frontend/src/pages/NotFound.tsx @@ -1,5 +1,5 @@ -import { Button } from 'tdesign-react'; import { useNavigate } from 'react-router'; +import { Button } from 'tdesign-react'; export default function NotFound() { const navigate = useNavigate(); diff --git a/frontend/src/pages/Providers/ModelTable.tsx b/frontend/src/pages/Providers/ModelTable.tsx index afb8a14..6b41eeb 100644 --- a/frontend/src/pages/Providers/ModelTable.tsx +++ b/frontend/src/pages/Providers/ModelTable.tsx @@ -1,7 +1,7 @@ import { Button, Table, Tag, Popconfirm, Space } from 'tdesign-react'; -import type { PrimaryTableCol } from 'tdesign-react/es/table/type'; -import type { Model } from '@/types'; import { useModels, useDeleteModel } from '@/hooks/useModels'; +import type { Model } from '@/types'; +import type { PrimaryTableCol } from 'tdesign-react/es/table/type'; interface ModelTableProps { providerId: string; diff --git a/frontend/src/pages/Providers/ProviderTable.tsx b/frontend/src/pages/Providers/ProviderTable.tsx index 6219bd5..1df29f4 100644 --- a/frontend/src/pages/Providers/ProviderTable.tsx +++ b/frontend/src/pages/Providers/ProviderTable.tsx @@ -1,7 +1,7 @@ import { Button, Table, Tag, Popconfirm, Space, Card } from 'tdesign-react'; -import type { PrimaryTableCol } from 'tdesign-react/es/table/type'; import type { Provider, Model } from '@/types'; import { ModelTable } from './ModelTable'; +import type { PrimaryTableCol } from 'tdesign-react/es/table/type'; interface ProviderTableProps { providers: Provider[]; diff --git a/frontend/src/pages/Providers/index.tsx b/frontend/src/pages/Providers/index.tsx index e35ed7e..b877e4e 100644 --- a/frontend/src/pages/Providers/index.tsx +++ b/frontend/src/pages/Providers/index.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; -import type { Provider, Model, UpdateProviderInput, UpdateModelInput } from '@/types'; -import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders'; import { useCreateModel, useUpdateModel } from '@/hooks/useModels'; -import { ProviderTable } from './ProviderTable'; -import { ProviderForm } from './ProviderForm'; +import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders'; +import type { Provider, Model, UpdateProviderInput, UpdateModelInput } from '@/types'; import { ModelForm } from './ModelForm'; +import { ProviderForm } from './ProviderForm'; +import { ProviderTable } from './ProviderTable'; export default function ProvidersPage() { const { data: providers = [], isLoading } = useProviders(); diff --git a/frontend/src/pages/Stats/StatCards.tsx b/frontend/src/pages/Stats/StatCards.tsx index 2275eea..7fc8340 100644 --- a/frontend/src/pages/Stats/StatCards.tsx +++ b/frontend/src/pages/Stats/StatCards.tsx @@ -1,5 +1,5 @@ -import { Row, Col, Card, Statistic } from 'tdesign-react'; import { ChartBarIcon, ChartLineIcon, ServerIcon, Calendar1Icon } from 'tdesign-icons-react'; +import { Row, Col, Card, Statistic } from 'tdesign-react'; import type { UsageStats } from '@/types'; interface StatCardsProps { diff --git a/frontend/src/pages/Stats/StatsTable.tsx b/frontend/src/pages/Stats/StatsTable.tsx index 81e8223..b6cd145 100644 --- a/frontend/src/pages/Stats/StatsTable.tsx +++ b/frontend/src/pages/Stats/StatsTable.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { Table, Select, Input, DateRangePicker, Space, Card } from 'tdesign-react'; -import type { PrimaryTableCol } from 'tdesign-react/es/table/type'; import type { UsageStats, Provider } from '@/types'; +import type { PrimaryTableCol } from 'tdesign-react/es/table/type'; interface StatsTableProps { providers: Provider[]; diff --git a/frontend/src/pages/Stats/UsageChart.tsx b/frontend/src/pages/Stats/UsageChart.tsx index 2eee761..f03ff3c 100644 --- a/frontend/src/pages/Stats/UsageChart.tsx +++ b/frontend/src/pages/Stats/UsageChart.tsx @@ -1,5 +1,5 @@ -import { Card } from 'tdesign-react'; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, ResponsiveContainer, Tooltip } from 'recharts'; +import { Card } from 'tdesign-react'; import type { UsageStats } from '@/types'; interface UsageChartProps { diff --git a/frontend/src/pages/Stats/index.tsx b/frontend/src/pages/Stats/index.tsx index 7c9c9e9..57f6449 100644 --- a/frontend/src/pages/Stats/index.tsx +++ b/frontend/src/pages/Stats/index.tsx @@ -2,8 +2,8 @@ import { useState, useMemo } from 'react'; import { useProviders } from '@/hooks/useProviders'; import { useStats } from '@/hooks/useStats'; import { StatCards } from './StatCards'; -import { UsageChart } from './UsageChart'; import { StatsTable } from './StatsTable'; +import { UsageChart } from './UsageChart'; export default function StatsPage() { const { data: providers = [] } = useProviders(); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 95dfed2..f2f30df 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,7 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' -import obfuscatorPlugin from 'vite-plugin-javascript-obfuscator' import path from 'node:path' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' +import obfuscatorPlugin from 'vite-plugin-javascript-obfuscator' const vendorChunks: Record = { 'vendor-react': ['react', 'react-dom', 'react-router'], diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index b4fa797..7756efe 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -1,6 +1,6 @@ -import { defineConfig } from 'vitest/config' -import react from '@vitejs/plugin-react' import path from 'node:path' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vitest/config' export default defineConfig({ plugins: [react()], diff --git a/openspec/specs/frontend-lint-rules/spec.md b/openspec/specs/frontend-lint-rules/spec.md new file mode 100644 index 0000000..1329830 --- /dev/null +++ b/openspec/specs/frontend-lint-rules/spec.md @@ -0,0 +1,114 @@ +# 前端 Lint 规则 + +## Purpose + +TBD - 定义前端 ESLint 规则配置、构建集成 lint 检查、以及自定义规则 + +## Requirements + +### Requirement: ESLint 规则配置 + +前端 SHALL 在 `eslint.config.js` 中配置以下规则: + +- `@tanstack/query/exhaustive-deps`: `error` — queryFn 中使用的变量 SHALL 出现在 queryKey 中 +- `@tanstack/query/no-void-query-fn`: `error` — queryFn SHALL 有返回值 +- `@tanstack/query/stable-query-client`: `error` — QueryClient SHALL NOT 在组件渲染中创建 +- `@tanstack/query/no-unstable-deps`: `error` — query 选项中 SHALL NOT 有不稳定引用 +- `@tanstack/query/infinite-query-property-order`: `error` — infinite query 属性顺序 SHALL 规范 +- `@tanstack/query/mutation-property-order`: `error` — mutation 回调顺序 SHALL 规范 +- `@tanstack/query/no-rest-destructuring`: `warn` — query 结果 SHALL NOT 使用 rest 解构 +- `no-console`: `['error', { allow: ['warn', 'error'] }]` — 代码中 SHALL NOT 使用 `console.log`、`console.info`、`console.debug` 等,仅允许 `console.warn` 和 `console.error` +- `@typescript-eslint/consistent-type-imports`: `['error', { prefer: 'type-imports', fixStyle: 'inline-type-imports' }]` — type import SHALL 使用内联风格 `import { type Foo }` +- `@typescript-eslint/no-non-null-assertion`: `error` — 代码中 SHALL NOT 使用 `foo!` 非空断言 + +#### Scenario: TanStack Query 规则未启用时构建失败 + +- **WHEN** `eslint.config.js` 中未配置 TanStack Query 的 `flat/recommended` 规则 +- **THEN** 前端构建 SHALL 失败 + +#### Scenario: 使用 console.log 时构建失败 + +- **WHEN** 源代码中出现 `console.log(...)` 调用 +- **THEN** ESLint SHALL 报告错误 +- **THEN** 前端构建 SHALL 失败 + +#### Scenario: 使用 console.warn 时不报错 + +- **WHEN** 源代码中出现 `console.warn(...)` 调用 +- **THEN** ESLint SHALL NOT 报告错误 + +#### Scenario: 使用独立 type import 时自动修复 + +- **WHEN** 源代码中出现 `import type { Foo } from 'module'` +- **THEN** `eslint --fix` SHALL 自动修复为 `import { type Foo } from 'module'` + +#### Scenario: 使用非空断言时构建失败 + +- **WHEN** 源代码中出现 `foo!.bar` 非空断言 +- **THEN** ESLint SHALL 报告错误 + +### Requirement: 构建集成 lint 检查 + +前端 SHALL 在 `build` 命令中集成 ESLint 检查。 + +#### Scenario: 构建时执行 lint + +- **WHEN** 执行 `bun run build` +- **THEN** 构建 SHALL 依次执行 `tsc -b`、`eslint .`、`vite build` +- **THEN** 若 `eslint .` 报告任何错误,构建 SHALL 中断 + +#### Scenario: lint 警告不中断构建 + +- **WHEN** `eslint .` 仅报告警告(无错误) +- **THEN** 构建 SHALL 继续执行 `vite build` + +#### Scenario: 单独执行 lint + +- **WHEN** 执行 `bun run lint` +- **THEN** SHALL 运行 `eslint .` + +#### Scenario: 自动修复 lint 问题 + +- **WHEN** 执行 `bun run lint:fix` +- **THEN** SHALL 运行 `eslint . --fix` + +### Requirement: 自定义规则禁止硬编码颜色 + +前端 SHALL 提供自定义 ESLint 规则 `no-hardcoded-color-in-style`,检测 JSX style 属性中的硬编码颜色值。 + +#### Scenario: 检测十六进制颜色 + +- **WHEN** JSX style 属性值匹配 `#xxx` 或 `#xxxxxx` 格式 +- **THEN** 规则 SHALL 报告警告 +- **THEN** 警告消息 SHALL 提示使用 `var(--td-*)` CSS Token + +#### Scenario: 检测 rgb/rgba/hsl 颜色函数 + +- **WHEN** JSX style 属性值匹配 `rgb()`、`rgba()`、`hsl()` 格式 +- **THEN** 规则 SHALL 报告警告 + +#### Scenario: 允许 CSS Token 引用 + +- **WHEN** JSX style 属性值为 `var(--td-*)` 格式 +- **THEN** 规则 SHALL NOT 报告 + +#### Scenario: 允许特殊颜色关键字 + +- **WHEN** JSX style 属性值为 `inherit`、`transparent`、`currentColor`、`none`、`unset`、`initial` +- **THEN** 规则 SHALL NOT 报告 + +#### Scenario: 允许数字值 + +- **WHEN** JSX style 属性值为数字(如 `0`、`16`) +- **THEN** 规则 SHALL NOT 报告 + +### Requirement: 自定义规则存放位置 + +自定义 ESLint 规则 SHALL 存放在 `frontend/eslint-rules/` 目录中。 + +#### Scenario: 自定义规则目录结构 + +- **WHEN** 添加自定义 ESLint 规则 +- **THEN** 规则文件 SHALL 放置在 `frontend/eslint-rules/` 目录下 +- **THEN** `eslint.config.js` SHALL 通过相对路径引用本地插件 +- **THEN** 自定义规则 SHALL NOT 作为 npm 包发布 diff --git a/openspec/specs/frontend/spec.md b/openspec/specs/frontend/spec.md index 0aa0403..a77e6e6 100644 --- a/openspec/specs/frontend/spec.md +++ b/openspec/specs/frontend/spec.md @@ -482,6 +482,9 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **THEN** TypeScript 配置 SHALL 开启 noUncheckedIndexedAccess - **THEN** 所有代码 SHALL NOT 使用 any 类型 - **THEN** tsconfig SHALL 合并为单文件(不使用 project references) +- **THEN** type import SHALL 使用内联风格 `import { type Foo }` +- **THEN** 代码 SHALL NOT 使用非空断言 `foo!` +- **THEN** 代码 SHALL NOT 使用 `console.log`、`console.info`、`console.debug`(仅允许 `console.warn` 和 `console.error`) #### Scenario: React 函数组件 @@ -505,6 +508,8 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 - **THEN** Vite SHALL 对业务代码执行混淆处理 - **THEN** 混淆 SHALL 仅应用于 src 目录下的业务代码 - **THEN** 混淆 SHALL NOT 应用于 node_modules 中的第三方库 +- **THEN** 构建流程 SHALL 在 vite build 之前执行 ESLint 检查 +- **THEN** ESLint 检查失败 SHALL 中断构建 ### Requirement: 与后端 API 通信