UNPKG

@kalimahapps/vue-icons

Version:

70,000+ SVG icons of popular icon sets that you can add seamlessly to vue projects

193 lines (192 loc) 6.72 kB
import * as vueParser from 'vue-eslint-parser'; import * as changeCase from 'change-case'; import MagicString from 'magic-string'; import { ICONS_PACKAGE_IMPORT_PATH, isIconName } from './helpers.js'; const { pascalCase } = changeCase; /** * Find icons in template Like <AnOutlinedSearch/> * * @param {Node} node Node to search * @param {Set<string>} usedIcons Set of used icon names * @return */ const findIconTagInTemplate = function (node, usedIcons) { if (node.type !== 'VElement' || !isIconName(node.rawName)) { return; } const pascalName = pascalCase(node.rawName, { mergeAmbiguousCharacters: true, }); usedIcons.add(pascalName); }; /** * Handle literal attributes cases like: * <ComponentName icon='icon-name'></ComponentName> * * @param {VAttribute | VDirective} attribute Attribute to process * @return {Modification[]} List of modifications */ const processLiteralAttributes = function (attribute) { if (attribute.type !== 'VAttribute' || attribute.key.type !== 'VIdentifier' || !attribute.value || attribute.value.type !== 'VLiteral' || !isIconName(attribute.value.value)) { return []; } const modifications = []; const colonToHyphen = attribute.value.value.replace(':', '-'); const pascalName = pascalCase(colonToHyphen, { mergeAmbiguousCharacters: true, }); const [valueStart, valueEnd] = attribute.value.range; // Skip the quotes around the value const startWithoutQuotes = valueStart + 1; const endWithoutQuotes = valueEnd - 1; modifications.push({ start: startWithoutQuotes, end: endWithoutQuotes, replacement: pascalName, type: 'icon', }); // Add : to attribute name const [keyStart, keyEnd] = attribute.key.range; const replacement = `:${attribute.key.name}`; modifications.push({ start: keyStart, end: keyEnd, replacement, type: 'attribute', }); return modifications; }; /** * Handle dynamic attributes cases like: * <ComponentName :icon="test ? 'icon-if-true' : 'icon-if-false'"></ComponentName> * * @param {VAttribute | VDirective} attribute Attribute to process * @return {Modification[]} List of modifications */ const processDynamicAttributes = function (attribute) { if (attribute.key.type !== 'VDirectiveKey' || !attribute.value || attribute.value.type !== 'VExpressionContainer' || attribute.value.expression?.type !== 'ConditionalExpression') { return []; } const modifications = []; const { consequent, alternate } = attribute.value.expression; for (const node of [consequent, alternate]) { if (node.type === 'Literal' && typeof node.value === 'string' && isIconName(node.value)) { const colonToHyphen = node.value.replace(':', '-'); const pascalName = pascalCase(colonToHyphen, { mergeAmbiguousCharacters: true, }); const [valueStart, valueEnd] = node.range; modifications.push({ start: valueStart, end: valueEnd, replacement: pascalName, type: 'icon', }); } } return modifications; }; /** * Find <VueIcon> components in the template * * @param {Node} node Node to search * @param {Modification[]} modifications Array to store modifications */ const findVueIconComponentInTemplate = function (node, modifications) { if (node.type !== 'VElement') { return; } for (const attribute of node.startTag.attributes) { const literals = processLiteralAttributes(attribute); if (literals.length > 0) { modifications.push(...literals); } const dynamic = processDynamicAttributes(attribute); if (dynamic.length > 0) { modifications.push(...dynamic); } } }; const detectAndReplaceTemplateIcons = function (templateAst, usedIcons, modifications) { vueParser.AST.traverseNodes(templateAst, { enterNode: function (node) { findVueIconComponentInTemplate(node, modifications); findIconTagInTemplate(node, usedIcons); }, leaveNode: function (node) { // No specific leave logic needed for now }, }); }; const detectAndReplaceScriptIcons = function (scriptAst, modifications) { vueParser.AST.traverseNodes(scriptAst, { enterNode: function (node) { if (node.type !== 'Literal' || typeof node.value !== 'string' || !isIconName(node.value)) { return; } const colonToHyphen = node.value.replace(':', '-'); const pascalName = pascalCase(colonToHyphen, { mergeAmbiguousCharacters: true, }); const [start, end] = node.range; modifications.push({ start, end, replacement: pascalName, type: 'icon', }); }, leaveNode: function (node) { // No specific leave logic needed for now }, }); }; const parseVueFiles = function (code) { const usedIcons = new Set(); const modifications = []; const { ast } = vueParser.parseForESLint(code, { ecmaVersion: 'latest', sourceType: 'module', parser: { ts: '@typescript-eslint/parser', }, }); const magicString = new MagicString(code); if (ast.templateBody) { detectAndReplaceTemplateIcons(ast.templateBody, usedIcons, modifications); } if (ast.body) { for (const node of ast.body) { detectAndReplaceScriptIcons(node, modifications); } } // Apply modifications in reverse order modifications.sort((a, b) => { return b.start - a.start; }); for (const modification of modifications) { const { start, end, replacement } = modification; magicString.overwrite(start, end, replacement); if (modification.type === 'icon') { usedIcons.add(replacement); } } if (usedIcons.size > 0) { let importStatement = `import { ${[...usedIcons].join(', ')} } from '${ICONS_PACKAGE_IMPORT_PATH}';\n`; const [start, end] = ast.range; // there is no script tag in the file, so create one if (start === end && start === 0) { importStatement = `<script setup>\n${importStatement}</script>\n`; } magicString.prependLeft(start, importStatement); } return magicString; }; export default parseVueFiles;