feat: 前端 ESLint 规则增强,自动检测 LLM 编码违规
- 启用 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 编写自定义规则集成测试
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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<string, unknown> | 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<string, unknown>;
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
124
frontend/src/__tests__/eslint-rules/no-hardcoded-color.test.ts
Normal file
124
frontend/src/__tests__/eslint-rules/no-hardcoded-color.test.ts
Normal file
@@ -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: `<div style={{ color: 'var(--td-text-color-placeholder)' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'numeric value 0',
|
||||
code: `<div style={{ opacity: 0 }} />`,
|
||||
},
|
||||
{
|
||||
name: 'numeric value 16',
|
||||
code: `<div style={{ width: 16 }} />`,
|
||||
},
|
||||
{
|
||||
name: 'inherit keyword',
|
||||
code: `<div style={{ color: 'inherit' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'transparent keyword',
|
||||
code: `<div style={{ color: 'transparent' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'currentColor keyword',
|
||||
code: `<div style={{ color: 'currentColor' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'none keyword',
|
||||
code: `<div style={{ display: 'none' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'unset keyword',
|
||||
code: `<div style={{ color: 'unset' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'initial keyword',
|
||||
code: `<div style={{ color: 'initial' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'pixel string value',
|
||||
code: `<div style={{ width: '100px' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'percentage string value',
|
||||
code: `<div style={{ width: '50%' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'plain numeric string value',
|
||||
code: `<div style={{ zIndex: '10' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'auto keyword',
|
||||
code: `<div style={{ width: 'auto' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'contain keyword',
|
||||
code: `<div style={{ backgroundSize: 'contain' }} />`,
|
||||
},
|
||||
{
|
||||
name: 'cover keyword',
|
||||
code: `<div style={{ backgroundSize: 'cover' }} />`,
|
||||
},
|
||||
],
|
||||
|
||||
invalid: [
|
||||
{
|
||||
name: 'hex3 color #fff',
|
||||
code: `<div style={{ color: '#fff' }} />`,
|
||||
errors: [{ messageId: 'hardcodedColor', data: { value: '#fff' } }],
|
||||
},
|
||||
{
|
||||
name: 'hex6 color #ffffff',
|
||||
code: `<div style={{ color: '#ffffff' }} />`,
|
||||
errors: [{ messageId: 'hardcodedColor', data: { value: '#ffffff' } }],
|
||||
},
|
||||
{
|
||||
name: 'hex8 color #ffffffff',
|
||||
code: `<div style={{ color: '#ffffffff' }} />`,
|
||||
errors: [{ messageId: 'hardcodedColor', data: { value: '#ffffffff' } }],
|
||||
},
|
||||
{
|
||||
name: 'rgb color',
|
||||
code: `<div style={{ color: 'rgb(255, 255, 255)' }} />`,
|
||||
errors: [{ messageId: 'hardcodedColor', data: { value: 'rgb(255, 255, 255)' } }],
|
||||
},
|
||||
{
|
||||
name: 'rgba color',
|
||||
code: `<div style={{ color: 'rgba(255, 255, 255, 0.5)' }} />`,
|
||||
errors: [{ messageId: 'hardcodedColor', data: { value: 'rgba(255, 255, 255, 0.5)' } }],
|
||||
},
|
||||
{
|
||||
name: 'hsl color',
|
||||
code: `<div style={{ color: 'hsl(120, 50%, 50%)' }} />`,
|
||||
errors: [{ messageId: 'hardcodedColor', data: { value: 'hsl(120, 50%, 50%)' } }],
|
||||
},
|
||||
{
|
||||
name: 'multiple style properties with one hardcoded',
|
||||
code: `<div style={{ width: 100, color: '#999', opacity: 0.5 }} />`,
|
||||
errors: [{ messageId: 'hardcodedColor', data: { value: '#999' } }],
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<string, string> = {
|
||||
duplicate_model: '同一供应商下模型名称已存在',
|
||||
|
||||
@@ -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<string, string> = {
|
||||
duplicate_model: '同一供应商下模型名称已存在',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user