// eslint-rules/no-hardcoded-design-tokens.js // 检测 TSX/TS 中硬编码的字体家族字面量(Inter/Fraunces/JetBrains Mono) // 白名单:src/app/styles/tokens/primitive.css(令牌定义)、email-channel.ts、manifest.ts // 使用单词边界匹配,避免误判 Interval/Interactive/Interrupt 等含 Inter 子串的标识符 const FORBIDDEN_FONTS = ['Inter', 'Fraunces', 'JetBrains Mono']; const WHITELIST_FILES = [ 'eslint-rules/no-hardcoded-design-tokens.js', 'src/app/styles/tokens/primitive.css', 'src/modules/notifications/channels/email-channel.ts', 'src/app/manifest.ts', ]; // 为每个字体名构建单词边界正则:\bInter\b 匹配 "Inter" 但不匹配 "Interval" const FORBIDDEN_PATTERNS = FORBIDDEN_FONTS.map((font) => ({ font, regex: new RegExp(`\\b${font}\\b`), })); module.exports = { meta: { type: 'problem', docs: { description: '禁止硬编码字体家族字面量,使用 var(--font-family-sans/serif/mono)', category: 'Best Practices', recommended: true, }, messages: { forbiddenFont: '禁止硬编码字体家族 "{{font}}",使用 var(--font-family-sans/serif/mono) 或 var(--font-family-mono)', }, schema: [], }, create(context) { const filename = context.getFilename().replace(/\\/g, '/'); if (WHITELIST_FILES.some((f) => filename.endsWith(f))) { return {}; } return { Literal(node) { if (typeof node.value !== 'string') return; for (const { font, regex } of FORBIDDEN_PATTERNS) { if (regex.test(node.value)) { context.report({ node, messageId: 'forbiddenFont', data: { font }, }); break; } } }, TemplateElement(node) { const raw = node.value.raw; for (const { font, regex } of FORBIDDEN_PATTERNS) { if (regex.test(raw)) { context.report({ node, messageId: 'forbiddenFont', data: { font }, }); break; } } }, }; }, };