const names = require('./names') const v5OnlyNames = require('./v5-only-names') const v5OnlyNamesSet = new Set(v5OnlyNames) const DEFAULTS = { whitelist: names, componentName: 'FontAwesome6', attributeName: 'name', }; function getJSXAttribute(openingElement, attrName) { return openingElement.attributes.find( (attr) => attr && attr.type === 'JSXAttribute' && attr.name && attr.name.name === attrName ); } function getStringLiteralValue(attribute) { if (!attribute || !attribute.value) return null; const val = attribute.value; // if (val.type === 'Literal' && typeof val.value === 'string') { return val.value; } // if ( val.type === 'JSXExpressionContainer' && val.expression && val.expression.type === 'Literal' && typeof val.expression.value === 'string' ) { return val.expression.value; } // template literal without expressions if ( val.type === 'JSXExpressionContainer' && val.expression && val.expression.type === 'TemplateLiteral' && val.expression.expressions.length === 0 ) { return val.expression.quasis[0]?.value?.cooked ?? null; } return null; } const replacements = { 'plus-circle': 'circle-plus', } module.exports = { meta: { type: 'problem', docs: { description: 'Ensure FontAwesome6 name prop is a string literal in whitelist', recommended: false, }, schema: [ { type: 'object', additionalProperties: false, properties: { whitelist: { type: 'array', items: { type: 'string' }, default: DEFAULTS.whitelist, }, componentName: { type: 'string', default: DEFAULTS.componentName, }, attributeName: { type: 'string', default: DEFAULTS.attributeName, }, caseSensitive: { type: 'boolean', default: true } }, }, ], messages: { invalidName: '{{componentName}} 中不存在图标 {{name}},{{suggestion}}', }, }, create(context) { const options = context.options && context.options[0] ? context.options[0] : {}; const componentName = options.componentName || DEFAULTS.componentName; const attributeName = options.attributeName || DEFAULTS.attributeName; const caseSensitive = options.caseSensitive ?? true; const whitelistRaw = Array.isArray(options.whitelist) ? options.whitelist : DEFAULTS.whitelist; const normalize = (s) => caseSensitive ? String(s) : String(s).toLowerCase(); const whitelist = new Set(whitelistRaw.map(normalize)); function isTargetComponent(node) { // Supports: , const nameNode = node.name; if (!nameNode) return false; if (nameNode.type === 'JSXIdentifier') { return nameNode.name === componentName; } if (nameNode.type === 'JSXMemberExpression') { // e.g., UI.FontAwesome6 let base = nameNode; while (base.type === 'JSXMemberExpression') { if (base.property && base.property.name === componentName) { return true; } base = base.object; } } return false; } return { JSXOpeningElement(opening) { if (!isTargetComponent(opening)) return; const attrNode = getJSXAttribute(opening, attributeName); if (!attrNode) return; const literal = getStringLiteralValue(attrNode); // Only lint when it's a string literal if (literal == null) return; const normalized = normalize(literal); if (!whitelist.has(normalized)) { context.report({ node: attrNode.value || attrNode, messageId: 'invalidName', data: { componentName, name: literal, suggestion: getSuggestion(normalized, literal), }, }); } }, }; }, }; function getSuggestion(name, originalName) { if (replacements[name]) { return `请更换为 ${replacements[name]}` } if (v5OnlyNamesSet.has(name)) { return `${originalName} 只能和 FontAwesome5 组件一起使用` } return '请更换为其他图标' }