/** * ESLint 配置 —— 只 lint renderer 业务代码(src/renderer, src/shared, src/preload)。 * * 不 lint src/main:那里是 Electron 主进程,跑的是 Node 18 的能力,规则集合不一样, * 引入会让 CI 噪声很大、价值很低。先把 renderer 这一层质量守住。 * * 主要规则选型: * - "@typescript-eslint/recommended-type-checked" 启用类型感知规则,副作用比 no-type-checked 系列少。 * - react/jsx-runtime(自动从 react/jsx-runtime 引入),不用 import React。 * - jsx-a11y 全套,覆盖 ARIA、键盘、对比度前的标签语义(颜色用 tailwind,不在 a11y 范围里)。 * - 自定义规则: * * no-restricted-syntax 禁止 console.* 在 src 代码里残留(要日志走 electron log); * * no-restricted-imports 禁掉 cross-process import(renderer 永远不该 import src/main)。 * * 故意没开:prettier(用 prettier --check)、import/order(争论太多收益太小)。 */ module.exports = { root: true, env: { browser: true, es2022: true, node: true }, parser: '@typescript-eslint/parser', parserOptions: { ecmaVersion: 'latest', sourceType: 'module', ecmaFeatures: { jsx: true }, project: ['./tsconfig.json'], tsconfigRootDir: __dirname }, settings: { react: { version: 'detect' } }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended-type-checked', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended', 'plugin:jsx-a11y/recommended' ], plugins: ['@typescript-eslint', 'react', 'react-hooks', 'jsx-a11y'], ignorePatterns: [ 'out/**', 'node_modules/**', 'dist/**', 'release/**', 'coverage/**', 'test-results/**', 'playwright-report/**', '*.config.cjs', '*.config.js', '*.config.ts', '.eslintrc.cjs', 'tailwind.config.cjs', 'postcss.config.cjs' ], rules: { // 显式 any 在这种规模的代码里其实有必要 —— @typescript-eslint/no-explicit-any 在 type-checked // 模式下噪声太大(每个 IpcError catch 都会撞)。先关掉,等真有滥用时再开。 '@typescript-eslint/no-explicit-any': 'off', // ts-ignore 用一行注释比挨个枚举类型便宜;只在违规真的不可避免时才允许 '@typescript-eslint/ban-ts-comment': ['warn', { 'ts-ignore': 'allow-with-description' }], // React 17+ 没用 React.* API,没必要强制默认写 React import 'react/react-in-jsx-scope': 'off', 'react/prop-types': 'off', // 性能优化 / a11y 都需要 conditional roles(比如 isSelected ? 'true' : undefined) 'react/jsx-boolean-value': ['warn', 'never'], // hooks 依赖数组:我们有 useEffect 用 useRef 当 token 守门,react-hooks 会误报 'react-hooks/exhaustive-deps': 'warn', // a11y 误报:role="button" 在 / 上是正确做法;jsx-a11y v6 仍把它当 interactive // 控件要求 tabIndex,规则偶尔会和组件本身定的 roving tabindex 打架 → warn 不 block 'jsx-a11y/no-noninteractive-element-interactions': 'warn', 'jsx-a11y/click-events-have-key-events': 'warn', // src/shared 是纯类型和常量,应该优先用 type 而不是 interface —— 同名 interface 合并 // 在 React 项目里几乎总是误用 '@typescript-eslint/consistent-type-imports': ['warn', { prefer: 'type-imports' }], // onClick={async () => await foo()} 在 React 里是常见且正确的写法 —— React 会忽略返回 // 的 Promise(这是 React 的设计缺陷,但用 ESLint 卡它没有收益,只会让所有按钮代码都 // 包一层 void 包装)。同理表单 onSubmit。 '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: { arguments: false, attributes: false } } ], 'no-restricted-syntax': [ 'warn', { // renderer 里允许 console.error 用于诊断,但不允许 console.log —— 那是 debug 残留 selector: "CallExpression[callee.object.name='console'][callee.property.name!=/^(error|warn)$/]", message: 'renderer 进程不要留 console.log/warn;日志走 main 进程的 electron-log。' } ], 'no-restricted-imports': [ 'error', { // 阻止 renderer 跨进程边界拉主进程代码 —— 一旦发生,contextBridge 的隔离就破了 patterns: [ { group: ['**/src/main/**', '**/main/**'], message: 'renderer 不允许 import src/main/;走 preload 暴露的 IPC 接口。' } ] } ] }, overrides: [ { // 测试文件允许 any 和未使用变量(fixture/dummy 经常违反)。console.log 也允许—— // 测试期间的诊断输出没法走 electron-log。require-await 在 mock fn 里大量误报 // (vi.fn().mockResolvedValue 总是返回 Promise,写成 async 是统一形式): files: ['**/*.test.{ts,tsx}', '**/__tests__/**', 'e2e/**'], rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], 'no-restricted-syntax': 'off', '@typescript-eslint/require-await': 'off' } }, { // e2e 测试禁止固定 sleep:waitForTimeout(>=1000) 几乎一定意味着「等某件事完成」, // 而这件事可以用 web-first 断言(expect().toBeVisible/toHaveText/toPass) // 表达成「等某件事真的发生」。固定 sleep 在 CI 慢的时候假阴、快的机器上又假阳。 // 例外:≤50ms 紧跟在 keyboard.press / type 后面,给 DOM 派发的事件循环时间, // 这种替代方案不如直接 sleep 直观。 files: ['e2e/**'], rules: { 'no-restricted-syntax': [ 'error', { selector: "CallExpression[callee.object.name='page'][callee.property.name='waitForTimeout'][arguments.0.value>=1000]", message: '禁止固定 sleep >=1s。用 web-first 断言代替(expect.toBeVisible/toPass 等),CI 慢机器才不会假阴、快的机器才不会假阳。' } ] } }, { files: ['src/preload/**'], // preload 在沙箱里跑,没有 Node DOM,但有 Node 全局 env: { browser: false, node: true } } ] }