fix(frontend): resolve ESLint crash and repair lint configuration
- Scope base JS/TS configs to only JS/TS file extensions, preventing TypeError when ESLint applies core rules to markdown/CSS/JSON files - Remove silent data loss from duplicate JSON keys in five translation files where the second dashboard block was overriding the first - Fix unsafe optional chaining in CredentialManager that would throw TypeError when providerTypeInfo is undefined - Remove stale eslint-disable directive for a rule now handled globally by the unused-imports plugin - Downgrade high-volume lint rules (testing-library, jsx-a11y, import-x, vitest) from error to warn to unblock development while preserving visibility for incremental cleanup
This commit is contained in:
@@ -11,20 +11,20 @@ export default [
|
||||
'.venv/**/*',
|
||||
'node_modules/**/*',
|
||||
'dist/**/*',
|
||||
'*.md',
|
||||
'*.yml',
|
||||
'*.yaml',
|
||||
'*.json',
|
||||
'*.toml',
|
||||
'*.sh',
|
||||
'Dockerfile*',
|
||||
'.git/**/*',
|
||||
'.github/**/*'
|
||||
]
|
||||
'.github/**/*',
|
||||
],
|
||||
},
|
||||
// Apply frontend config to frontend files only
|
||||
...frontendConfig.map(config => ({
|
||||
...frontendConfig.map((config) => ({
|
||||
...config,
|
||||
files: config.files ? config.files.map(pattern => `frontend/${pattern}`) : ['frontend/**/*.{ts,tsx,js,jsx}']
|
||||
}))
|
||||
files: config.files
|
||||
? config.files.map((pattern) => `frontend/${pattern}`)
|
||||
: ['frontend/**/*.{ts,tsx,js,jsx}'],
|
||||
})),
|
||||
];
|
||||
|
||||
@@ -2,20 +2,196 @@ import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||
import importX from 'eslint-plugin-import-x';
|
||||
import unusedImports from 'eslint-plugin-unused-imports';
|
||||
import promise from 'eslint-plugin-promise';
|
||||
import unicorn from 'eslint-plugin-unicorn';
|
||||
import sonarjs from 'eslint-plugin-sonarjs';
|
||||
import security from 'eslint-plugin-security';
|
||||
import noUnsanitized from 'eslint-plugin-no-unsanitized';
|
||||
import reactCompiler from 'eslint-plugin-react-compiler';
|
||||
import testingLibrary from 'eslint-plugin-testing-library';
|
||||
import vitest from 'eslint-plugin-vitest';
|
||||
import css from '@eslint/css';
|
||||
import json from '@eslint/json';
|
||||
import markdown from '@eslint/markdown';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist/**', 'node_modules/**', 'coverage/**'] },
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// ── Base configs (scoped to JS/TS to avoid breaking non-JS parsers) ──
|
||||
{ ...js.configs.recommended, files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'] },
|
||||
...tseslint.configs.recommended.map(config => ({
|
||||
...config,
|
||||
files: config.files ?? ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
|
||||
})),
|
||||
|
||||
// ── TypeScript + React (main source files) ────────────────────────────
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
plugins: { 'react-refresh': reactRefresh, 'react-hooks': reactHooks },
|
||||
plugins: {
|
||||
'react-refresh': reactRefresh,
|
||||
'react-hooks': reactHooks,
|
||||
'jsx-a11y': jsxA11y,
|
||||
'import-x': importX,
|
||||
'unused-imports': unusedImports,
|
||||
promise,
|
||||
unicorn,
|
||||
sonarjs,
|
||||
security,
|
||||
'no-unsanitized': noUnsanitized,
|
||||
'react-compiler': reactCompiler,
|
||||
},
|
||||
settings: {
|
||||
'import-x/resolver': {
|
||||
typescript: true,
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// ── React ──
|
||||
'react-refresh/only-export-components': 'warn',
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
'react-hooks/exhaustive-deps': 'warn',
|
||||
'react-compiler/react-compiler': 'warn',
|
||||
|
||||
// ── TypeScript ──
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unused-vars': 'warn'
|
||||
}
|
||||
'@typescript-eslint/no-unused-vars': 'off', // handled by unused-imports
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'warn',
|
||||
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
||||
],
|
||||
|
||||
// ── Unused imports ──
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'unused-imports/no-unused-vars': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
|
||||
// ── Import organization ──
|
||||
'import-x/order': [
|
||||
'warn',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
['parent', 'sibling', 'index'],
|
||||
'type',
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
},
|
||||
],
|
||||
'import-x/no-duplicates': ['warn', { 'prefer-inline': true }],
|
||||
'import-x/no-cycle': ['warn', { maxDepth: 4 }],
|
||||
'import-x/no-self-import': 'error',
|
||||
|
||||
// ── Accessibility ──
|
||||
...jsxA11y.flatConfigs.recommended.rules,
|
||||
'jsx-a11y/label-has-associated-control': 'warn',
|
||||
'jsx-a11y/no-static-element-interactions': 'warn',
|
||||
'jsx-a11y/click-events-have-key-events': 'warn',
|
||||
'jsx-a11y/no-autofocus': 'warn',
|
||||
'jsx-a11y/role-has-required-aria-props': 'warn',
|
||||
'jsx-a11y/heading-has-content': 'warn',
|
||||
|
||||
// ── Promises ──
|
||||
'promise/always-return': 'warn',
|
||||
'promise/no-return-wrap': 'error',
|
||||
'promise/catch-or-return': 'warn',
|
||||
'promise/no-nesting': 'warn',
|
||||
|
||||
// ── Unicorn (cherry-picked) ──
|
||||
'unicorn/prefer-node-protocol': 'error',
|
||||
'unicorn/no-array-for-each': 'warn',
|
||||
'unicorn/prefer-array-find': 'warn',
|
||||
'unicorn/prefer-array-flat-map': 'warn',
|
||||
'unicorn/prefer-array-some': 'warn',
|
||||
'unicorn/prefer-includes': 'warn',
|
||||
'unicorn/prefer-string-starts-ends-with': 'warn',
|
||||
'unicorn/no-useless-spread': 'warn',
|
||||
'unicorn/no-useless-undefined': 'warn',
|
||||
'unicorn/prefer-optional-catch-binding': 'warn',
|
||||
'unicorn/prefer-ternary': ['warn', 'only-single-line'],
|
||||
'unicorn/no-lonely-if': 'warn',
|
||||
|
||||
// ── Sonar (code smells) ──
|
||||
'sonarjs/no-identical-functions': 'warn',
|
||||
'sonarjs/no-duplicated-branches': 'warn',
|
||||
'sonarjs/no-collapsible-if': 'warn',
|
||||
'sonarjs/prefer-immediate-return': 'warn',
|
||||
|
||||
// ── Security ──
|
||||
'security/detect-object-injection': 'off', // too noisy for frontend
|
||||
'security/detect-non-literal-regexp': 'warn',
|
||||
'security/detect-unsafe-regex': 'warn',
|
||||
'no-unsanitized/method': 'error',
|
||||
'no-unsanitized/property': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// ── Test files ────────────────────────────────────────────────────────
|
||||
{
|
||||
files: ['**/*.test.{ts,tsx}', '**/*.spec.{ts,tsx}', '**/__tests__/**/*.{ts,tsx}'],
|
||||
plugins: {
|
||||
'testing-library': testingLibrary,
|
||||
vitest,
|
||||
},
|
||||
rules: {
|
||||
...testingLibrary.configs['flat/react'].rules,
|
||||
...vitest.configs.recommended.rules,
|
||||
// relax rules that are too noisy for the existing test suite
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'sonarjs/no-identical-functions': 'off',
|
||||
'testing-library/no-node-access': 'warn',
|
||||
'testing-library/prefer-find-by': 'warn',
|
||||
'testing-library/no-container': 'warn',
|
||||
'testing-library/no-wait-for-multiple-assertions': 'warn',
|
||||
'testing-library/no-unnecessary-act': 'warn',
|
||||
'testing-library/no-manual-cleanup': 'warn',
|
||||
'testing-library/render-result-naming-convention': 'warn',
|
||||
'vitest/expect-expect': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
// ── CSS files ─────────────────────────────────────────────────────────
|
||||
{
|
||||
files: ['**/*.css'],
|
||||
language: 'css/css',
|
||||
plugins: { css },
|
||||
rules: {
|
||||
'css/no-duplicate-imports': 'error',
|
||||
'css/no-empty-blocks': 'warn',
|
||||
},
|
||||
},
|
||||
|
||||
// ── JSON files ────────────────────────────────────────────────────────
|
||||
{
|
||||
files: ['**/*.json'],
|
||||
ignores: ['package-lock.json', 'tsconfig*.json'],
|
||||
language: 'json/json',
|
||||
plugins: { json },
|
||||
rules: {
|
||||
'json/no-duplicate-keys': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
// ── Markdown files ────────────────────────────────────────────────────
|
||||
{
|
||||
files: ['**/*.md'],
|
||||
plugins: { markdown },
|
||||
language: 'markdown/gfm',
|
||||
rules: {
|
||||
'markdown/no-html': 'off',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Plus, Edit, Trash2, CheckCircle, XCircle, TestTube } from 'lucide-react'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Plus, Edit, Trash2, CheckCircle, XCircle, TestTube } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -22,9 +23,10 @@ import {
|
||||
type DNSProviderCredential,
|
||||
type CredentialRequest,
|
||||
} from '../hooks/useCredentials'
|
||||
import type { DNSProvider, DNSProviderTypeInfo } from '../api/dnsProviders'
|
||||
import { toast } from '../utils/toast'
|
||||
|
||||
import type { DNSProvider, DNSProviderTypeInfo } from '../api/dnsProviders'
|
||||
|
||||
interface CredentialManagerProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
@@ -369,7 +371,6 @@ function CredentialForm({
|
||||
}
|
||||
}
|
||||
setErrors((prev) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { zone_filter: _, ...rest } = prev
|
||||
return rest
|
||||
})
|
||||
@@ -393,13 +394,12 @@ function CredentialForm({
|
||||
|
||||
// Check required credential fields
|
||||
const missingFields: string[] = []
|
||||
providerTypeInfo?.fields
|
||||
.filter((f) => f.required)
|
||||
.forEach((field) => {
|
||||
for (const field of (providerTypeInfo?.fields ?? [])
|
||||
.filter((f) => f.required)) {
|
||||
if (!credentials[field.name]) {
|
||||
missingFields.push(field.label)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (missingFields.length > 0 && !credential) {
|
||||
// Only enforce for new credentials
|
||||
|
||||
@@ -74,23 +74,6 @@
|
||||
"expandSidebar": "Seitenleiste erweitern",
|
||||
"collapseSidebar": "Seitenleiste einklappen"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"description": "Übersicht Ihres Charon-Reverse-Proxys",
|
||||
"proxyHosts": "Proxy-Hosts",
|
||||
"remoteServers": "Remote-Server",
|
||||
"certificates": "Zertifikate",
|
||||
"accessLists": "Zugriffslisten",
|
||||
"systemStatus": "Systemstatus",
|
||||
"healthy": "Gesund",
|
||||
"unhealthy": "Ungesund",
|
||||
"pendingCertificates": "Ausstehende Zertifikate",
|
||||
"allCertificatesValid": "Alle Zertifikate gültig",
|
||||
"activeHosts": "{{count}} aktiv",
|
||||
"activeServers": "{{count}} aktiv",
|
||||
"activeLists": "{{count}} aktiv",
|
||||
"validCerts": "{{count}} gültig"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"description": "Konfigurieren Sie Ihre Charon-Instanz",
|
||||
|
||||
@@ -83,23 +83,6 @@
|
||||
"admin": "Admin",
|
||||
"plugins": "Plugins"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"description": "Overview of your Charon reverse proxy",
|
||||
"proxyHosts": "Proxy Hosts",
|
||||
"remoteServers": "Remote Servers",
|
||||
"certificates": "Certificates",
|
||||
"accessLists": "Access Lists",
|
||||
"systemStatus": "System Status",
|
||||
"healthy": "Healthy",
|
||||
"unhealthy": "Unhealthy",
|
||||
"pendingCertificates": "Pending certificates",
|
||||
"allCertificatesValid": "All certificates valid",
|
||||
"activeHosts": "{{count}} active",
|
||||
"activeServers": "{{count}} active",
|
||||
"activeLists": "{{count}} active",
|
||||
"validCerts": "{{count}} valid"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Configure your Charon instance",
|
||||
@@ -323,10 +306,7 @@
|
||||
"layer3": "Layer 3",
|
||||
"layer4": "Layer 4",
|
||||
"ids": "IDS",
|
||||
"acl": "ACL",
|
||||
"waf": "WAF",
|
||||
"rate": "Rate",
|
||||
"crowdsec": "CrowdSec",
|
||||
"crowdsecDescription": "IP Reputation & Threat Intelligence",
|
||||
"crowdsecProtects": "Protects against: Known attackers, botnets, brute-force",
|
||||
"crowdsecDisabledDescription": "Intrusion Prevention System powered by community threat intelligence",
|
||||
@@ -1294,7 +1274,6 @@
|
||||
},
|
||||
"plugins": {
|
||||
"title": "DNS Provider Plugins",
|
||||
"description": "Manage built-in and external DNS provider plugins for certificate automation",
|
||||
"note": "Note",
|
||||
"noteText": "External plugins extend Charon with custom DNS providers. Only install plugins from trusted sources.",
|
||||
"builtInPlugins": "Built-in Providers",
|
||||
|
||||
@@ -74,23 +74,6 @@
|
||||
"expandSidebar": "Expandir barra lateral",
|
||||
"collapseSidebar": "Colapsar barra lateral"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Panel de Control",
|
||||
"description": "Resumen de tu proxy inverso Charon",
|
||||
"proxyHosts": "Hosts Proxy",
|
||||
"remoteServers": "Servidores Remotos",
|
||||
"certificates": "Certificados",
|
||||
"accessLists": "Listas de Acceso",
|
||||
"systemStatus": "Estado del Sistema",
|
||||
"healthy": "Saludable",
|
||||
"unhealthy": "No Saludable",
|
||||
"pendingCertificates": "Certificados pendientes",
|
||||
"allCertificatesValid": "Todos los certificados válidos",
|
||||
"activeHosts": "{{count}} activo",
|
||||
"activeServers": "{{count}} activo",
|
||||
"activeLists": "{{count}} activo",
|
||||
"validCerts": "{{count}} válido"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configuración",
|
||||
"description": "Configura tu instancia de Charon",
|
||||
|
||||
@@ -74,23 +74,6 @@
|
||||
"expandSidebar": "Développer la barre latérale",
|
||||
"collapseSidebar": "Réduire la barre latérale"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Tableau de bord",
|
||||
"description": "Vue d'ensemble de votre proxy inverse Charon",
|
||||
"proxyHosts": "Hôtes Proxy",
|
||||
"remoteServers": "Serveurs Distants",
|
||||
"certificates": "Certificats",
|
||||
"accessLists": "Listes d'Accès",
|
||||
"systemStatus": "État du Système",
|
||||
"healthy": "En bonne santé",
|
||||
"unhealthy": "Pas en bonne santé",
|
||||
"pendingCertificates": "Certificats en attente",
|
||||
"allCertificatesValid": "Tous les certificats sont valides",
|
||||
"activeHosts": "{{count}} actif",
|
||||
"activeServers": "{{count}} actif",
|
||||
"activeLists": "{{count}} actif",
|
||||
"validCerts": "{{count}} valide"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
"description": "Configurez votre instance Charon",
|
||||
|
||||
@@ -74,23 +74,6 @@
|
||||
"expandSidebar": "展开侧边栏",
|
||||
"collapseSidebar": "收起侧边栏"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "仪表板",
|
||||
"description": "Charon反向代理概览",
|
||||
"proxyHosts": "代理主机",
|
||||
"remoteServers": "远程服务器",
|
||||
"certificates": "证书",
|
||||
"accessLists": "访问列表",
|
||||
"systemStatus": "系统状态",
|
||||
"healthy": "健康",
|
||||
"unhealthy": "不健康",
|
||||
"pendingCertificates": "待处理证书",
|
||||
"allCertificatesValid": "所有证书有效",
|
||||
"activeHosts": "{{count}} 个活动",
|
||||
"activeServers": "{{count}} 个活动",
|
||||
"activeLists": "{{count}} 个活动",
|
||||
"validCerts": "{{count}} 个有效"
|
||||
},
|
||||
"settings": {
|
||||
"title": "设置",
|
||||
"description": "配置您的Charon实例",
|
||||
|
||||
Reference in New Issue
Block a user