From 81050767da256d4f20749441ca4376717a9d86dd Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 6 Mar 2026 11:23:00 -0800 Subject: [PATCH] feat(admin-web): add ESLint flat config with Clean Architecture layer boundary enforcement - eslint.config.mjs: ESLint 9 flat config with per-layer no-restricted-imports rules - Domain: no outward deps; Infrastructure: domain only; Application: domain+infra; Store: domain only; Presentation: no direct infra access - Fix no-explicit-any in use-upload.ts (use unknown + type assertion) - Add lint:boundaries npm script for CI enforcement Co-Authored-By: Claude Sonnet 4.6 --- frontend/admin-web/eslint.config.mjs | 111 ++++++++++++++++++ frontend/admin-web/package.json | 1 + .../views/app-versions/hooks/use-upload.ts | 8 +- 3 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 frontend/admin-web/eslint.config.mjs diff --git a/frontend/admin-web/eslint.config.mjs b/frontend/admin-web/eslint.config.mjs new file mode 100644 index 0000000..2aed852 --- /dev/null +++ b/frontend/admin-web/eslint.config.mjs @@ -0,0 +1,111 @@ +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ baseDirectory: __dirname }); + +// ── Clean Architecture layer boundary rules ────────────────── +// Domain → nothing from this project +// Infrastructure → domain only +// Application → domain + infrastructure +// Store → domain only +// Views/Components → application + store + domain (NOT infrastructure) + +const LAYERS = { + domain: ['@/domain/*', '*/domain/*'], + infrastructure: ['@/infrastructure/*', '*/infrastructure/*'], + application: ['@/application/*', '*/application/*'], + store: ['@/store/*', '*/store/*'], + views: ['@/views/*', '*/views/*'], +}; + +function noImport(patterns, message) { + return patterns.map((group) => ({ group: [group], message })); +} + +const config = [ + ...compat.extends('next/core-web-vitals', 'next/typescript'), + + // ── Domain: pure entities + interfaces, zero outward deps ── + { + files: ['src/domain/**/*.ts', 'src/domain/**/*.tsx'], + rules: { + 'no-restricted-imports': ['error', { + patterns: [ + ...noImport(LAYERS.infrastructure, 'Domain must not import Infrastructure'), + ...noImport(LAYERS.application, 'Domain must not import Application'), + ...noImport(LAYERS.store, 'Domain must not import Store'), + ...noImport(LAYERS.views, 'Domain must not import Views'), + ], + }], + }, + }, + + // ── Infrastructure: repos + HTTP client, depends on domain only ── + { + files: ['src/infrastructure/**/*.ts', 'src/infrastructure/**/*.tsx'], + rules: { + 'no-restricted-imports': ['error', { + patterns: [ + ...noImport(LAYERS.application, 'Infrastructure must not import Application'), + ...noImport(LAYERS.store, 'Infrastructure must not import Store'), + ...noImport(LAYERS.views, 'Infrastructure must not import Views'), + ], + }], + }, + }, + + // ── Application (use cases): depends on domain + infrastructure ── + { + files: ['src/application/**/*.ts', 'src/application/**/*.tsx'], + rules: { + 'no-restricted-imports': ['error', { + patterns: [ + ...noImport(LAYERS.store, 'Application must not import Store'), + ...noImport(LAYERS.views, 'Application must not import Views'), + ], + }], + }, + }, + + // ── Store (Redux + Zustand): UI state only, depends on domain ── + { + files: ['src/store/**/*.ts', 'src/store/**/*.tsx'], + rules: { + 'no-restricted-imports': ['error', { + patterns: [ + ...noImport(LAYERS.infrastructure, 'Store must not import Infrastructure'), + ...noImport(LAYERS.application, 'Store must not import Application'), + ...noImport(LAYERS.views, 'Store must not import Views'), + ], + }], + }, + }, + + // ── Presentation (views + components + layouts): no direct infra access ── + { + files: [ + 'src/views/**/*.ts', + 'src/views/**/*.tsx', + 'src/components/**/*.ts', + 'src/components/**/*.tsx', + 'src/layouts/**/*.ts', + 'src/layouts/**/*.tsx', + ], + rules: { + 'no-restricted-imports': ['error', { + patterns: [ + ...noImport( + LAYERS.infrastructure, + 'Presentation must not import Infrastructure directly — use Application use-cases', + ), + ], + }], + }, + }, +]; + +export default config; diff --git a/frontend/admin-web/package.json b/frontend/admin-web/package.json index 4f173fe..2279e25 100644 --- a/frontend/admin-web/package.json +++ b/frontend/admin-web/package.json @@ -9,6 +9,7 @@ "start": "next start", "lint": "next lint", "lint:fix": "next lint --fix", + "lint:boundaries": "eslint src/domain src/infrastructure src/application src/store src/views src/components src/layouts --max-warnings 0", "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"", "type-check": "tsc --noEmit" diff --git a/frontend/admin-web/src/views/app-versions/hooks/use-upload.ts b/frontend/admin-web/src/views/app-versions/hooks/use-upload.ts index 73ac7be..ccaecdb 100644 --- a/frontend/admin-web/src/views/app-versions/hooks/use-upload.ts +++ b/frontend/admin-web/src/views/app-versions/hooks/use-upload.ts @@ -29,8 +29,8 @@ export function useUpload(appType: AppType, onSuccess: () => void) { if (info?.versionName) store.setVersionName(info.versionName); if (info?.versionCode) store.setBuildNumber(String(info.versionCode)); if (info?.minSdkVersion) store.setMinOsVersion(info.minSdkVersion); - } catch (err: any) { - console.warn('[useUpload] Parse failed:', err?.message); + } catch (err) { + console.warn('[useUpload] Parse failed:', (err as Error)?.message); store.setParseWarning('无法自动解析安装包信息,请手动填写版本号'); } store.setIsParsing(false); @@ -56,9 +56,9 @@ export function useUpload(appType: AppType, onSuccess: () => void) { console.log('[useUpload] Success:', result); store.reset(); onSuccess(); - } catch (err: any) { + } catch (err) { console.error('[useUpload] Failed:', err); - store.setUploadError(err?.message || 'Upload failed'); + store.setUploadError((err as Error)?.message || 'Upload failed'); } store.setIsUploading(false); };