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 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-06 11:23:00 -08:00
parent e92059fc75
commit 81050767da
3 changed files with 116 additions and 4 deletions

View File

@ -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;

View File

@ -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"

View File

@ -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);
};