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:
parent
e92059fc75
commit
81050767da
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue