const js = require('@eslint/js'); const tseslint = require('typescript-eslint'); const react = require('eslint-plugin-react'); const reactHooks = require('eslint-plugin-react-hooks'); const globals = require('globals'); /** * 自定义规则:禁止在组件/业务代码内写死颜色字面量。 * 对齐 AGENTS.md 配色禁令——颜色必须统一从 src/theme/colors.ts 导入。 * * 覆盖两类写法: * · 十六进制:#RGB / #RGBA / #RRGGBB / #RRGGBBAA * · 函数式:rgb() / rgba() / hsl() / hsla() * 后者是补上的**盲区**:规则原先只匹配纯 hex,于是 `rgba(0, 0, 0, 0.4)` 这类写死颜色 * 一路溜过门槛,最终表现为「同一个弹层遮罩在两个页面是两个透明度」——token 明明就在 * `colors.scrim`,全仓 9 处在用,唯独那一处自己重新推导了一遍(见 4.14)。 * 同时覆盖模板字面量,避免用 `` `rgba(0,0,0,${a})` `` 绕过去。 * 例外:theme/colors.ts 本身是设计令牌唯一来源,豁免。 */ const noHardcodedHexColor = { meta: { type: 'problem', docs: { description: '禁止写死颜色(hex / rgb / rgba / hsl),统一从 src/theme/colors.ts 导入', }, schema: [], messages: { noHex: '禁止写死颜色字面量 "{{value}}",请从 src/theme/colors.ts 导入颜色 token。', }, }, create(context) { const hexRe = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/; const fnRe = /(^|[\s,:;(])(rgb|rgba|hsl|hsla)\(/i; const isHardcodedColor = (value) => hexRe.test(value) || fnRe.test(value); return { Literal(node) { if (typeof node.value === 'string' && isHardcodedColor(node.value)) { context.report({ node, messageId: 'noHex', data: { value: node.value } }); } }, TemplateElement(node) { // 模板字面量的静态片段(`rgba(0,0,0,${a})` 的 raw 是 "rgba(0,0,0,") const raw = node.value.raw; if (typeof raw === 'string' && isHardcodedColor(raw)) { context.report({ node, messageId: 'noHex', data: { value: raw } }); } }, }; }, }; module.exports = tseslint.config( { ignores: [ 'node_modules/**', '.expo/**', 'dist/**', 'coverage/**', 'figma_out/**', '**/*.mjs', 'figma_discover.py', '**/__tests__/**', ], }, js.configs.recommended, ...tseslint.configs.recommended, { files: ['**/*.{ts,tsx}'], plugins: { react, 'react-hooks': reactHooks, local: { rules: { 'no-hardcoded-hex-color': noHardcodedHexColor } }, }, languageOptions: { parser: tseslint.parser, parserOptions: { ecmaFeatures: { jsx: true } }, sourceType: 'module', globals: { ...globals.node, ...globals.react }, }, settings: { react: { version: 'detect' } }, rules: { ...react.configs.recommended.rules, ...reactHooks.configs.recommended.rules, 'react/react-in-jsx-scope': 'off', 'react/jsx-uses-react': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, ], '@typescript-eslint/no-explicit-any': 'warn', 'no-empty': ['error', { allowEmptyCatch: true }], 'local/no-hardcoded-hex-color': 'error', }, }, { // 项目配置文件为 CommonJS,含 require / module / process files: ['eslint.config.js', 'babel.config.js', 'jest.config.js', 'app.config.js', '*.cjs'], languageOptions: { sourceType: 'commonjs', globals: { ...globals.node }, }, rules: { 'no-undef': 'off', '@typescript-eslint/no-require-imports': 'off', '@typescript-eslint/no-explicit-any': 'off', }, }, { // 设计令牌文件是颜色唯一来源,豁免 hex 检查 files: ['**/theme/**/*.ts'], rules: { 'local/no-hardcoded-hex-color': 'off' }, }, );