Last active
July 28, 2022 15:08
-
-
Save sergeysova/b6acdb75fc4a97aec6d451687f76955f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as generator from '@babel/generator'; | |
import * as t from '@babel/types'; | |
import {createFilter} from '@rollup/pluginutils'; | |
import changeCase from 'change-case'; | |
import fs from 'fs'; | |
import path from 'path'; | |
import {optimize} from 'svgo'; | |
import {fromXml} from 'xast-util-from-xml'; | |
const generate = generator.default.default; | |
export function viteSvgf({include = '**/*.svg', exclude, svgoPresetOverrides = {}} = {}) { | |
const filter = createFilter(include, exclude); | |
return { | |
name: 'vite-plugin-forest-svgf', | |
async transform(code, id) { | |
if (filter(id)) { | |
const svgSource = await fs.promises.readFile(id, 'utf8'); | |
const optimizedSvg = optimize(svgSource, { | |
path: id, | |
plugins: [ | |
{ | |
name: 'preset-default', | |
params: { | |
overrides: svgoPresetOverrides, | |
}, | |
}, | |
], | |
}); | |
if (optimizedSvg.error) { | |
throw optimizedSvg.error; | |
} | |
const svgAst = fromXml(optimizedSvg.data); | |
const changedAst = transformAst(svgAst, {id, defaultExport: true}); | |
return { | |
code: generate(changedAst).code, | |
map: null, | |
}; | |
} | |
}, | |
}; | |
} | |
function transformAst(tree, {id, defaultExport = false}) { | |
const options = {defaultExport}; | |
const name = intoComponentName(id); | |
const context = {id, name, options, importText: false}; | |
return compile(tree, context); | |
} | |
function intoComponentName(id) { | |
const fileName = path | |
.basename(id) | |
.replace(/\.svg/gi, '') | |
.replace(/^[0-9]+/, ''); | |
return changeCase.pascalCase(fileName); | |
} | |
function compile(tree, context) { | |
switch (tree.type) { | |
case 'root': { | |
return createRoot(compileChildren(tree, context), context); | |
} | |
case 'instruction': | |
case 'doctype': | |
break; | |
case 'text': | |
return createText(tree, context); | |
case 'element': | |
return createElement(tree, context); | |
} | |
} | |
function createText(element, context) { | |
if (element.value.trim().length === 0) { | |
return; | |
} | |
context.importText = true; | |
return textCall(element.value); | |
} | |
function compileChildren(element, context) { | |
if (element.children) return element.children.map((element) => compile(element, context)).filter(Boolean); | |
return []; | |
} | |
function hasAttributes(element) { | |
return element.attributes && Object.keys(element.attributes).length > 0; | |
} | |
function attributesIntoObject(attributes) { | |
const attrProperties = Object.entries(attributes).map(([key, value]) => | |
t.objectProperty(t.stringLiteral(key), t.stringLiteral(value)), | |
); | |
return t.objectExpression(attrProperties); | |
} | |
function attributesIntoSpecCall(attributes) { | |
const attrObject = attributesIntoObject(attributes); | |
const specObject = t.objectExpression([t.objectProperty(t.identifier('attr'), attrObject)]); | |
return callSpec([specObject]); | |
} | |
const importSpec = (name) => t.importSpecifier(t.identifier(name), t.identifier(name)); | |
const propsFn = t.memberExpression(t.identifier('props'), t.identifier('fn')); | |
const callSpec = (args) => t.expressionStatement(t.callExpression(t.identifier('spec'), args)); | |
const callSpecOnProps = callSpec([t.identifier('props')]); | |
const ifFnPresentCallIt = t.ifStatement(propsFn, t.expressionStatement(t.callExpression(propsFn, []))); | |
const callH = (args) => t.expressionStatement(t.callExpression(t.identifier('h'), args)); | |
const callback = (body) => t.functionExpression(t.identifier('fn'), [], t.blockStatement(body)); | |
const objectAttrsFn = (attrs, fnExpressions) => | |
t.objectExpression( | |
[ | |
attrs ? t.objectProperty(t.identifier('attr'), attrs) : null, | |
fnExpressions.length ? t.objectMethod('method', t.identifier('fn'), [], t.blockStatement(fnExpressions)) : null, | |
].filter(Boolean), | |
); | |
const textCall = (text) => | |
t.expressionStatement( | |
t.taggedTemplateExpression(t.identifier('text'), t.templateLiteral([t.templateElement({raw: text})], [])), | |
); | |
function createElementSvg(element, context) { | |
const elementBodyExpressions = []; | |
if (hasAttributes(element)) { | |
elementBodyExpressions.push(attributesIntoSpecCall(element.attributes)); | |
} | |
elementBodyExpressions.push(callSpecOnProps, ifFnPresentCallIt); | |
const childrenExpressions = compileChildren(element, context); | |
if (childrenExpressions.length) { | |
childrenExpressions.forEach((childrenExpr) => elementBodyExpressions.push(childrenExpr)); | |
} | |
return callH([t.stringLiteral(element.name), callback(elementBodyExpressions)]); | |
} | |
function createElementGeneric(element, context) { | |
return callH([ | |
t.stringLiteral(element.name), | |
objectAttrsFn( | |
hasAttributes(element) ? attributesIntoObject(element.attributes) : null, | |
compileChildren(element, context), | |
), | |
]); | |
} | |
function createElement(element, context) { | |
if (isSvg(element)) return createElementSvg(element, context); | |
return createElementGeneric(element, context); | |
} | |
function isSvg(element) { | |
return element.name === 'svg'; | |
} | |
function createRoot(body, context) { | |
const importSpecifiers = [importSpec('h'), importSpec('spec')]; | |
if (context.importText) { | |
importSpecifiers.push(importSpec('text')); | |
} | |
const imports = [t.importDeclaration(importSpecifiers, t.stringLiteral('forest'))]; | |
const functionExport = createFunctionExport( | |
t.functionDeclaration( | |
t.identifier(context.options.defaultExport ? context.name : 'Icon'), | |
[t.assignmentPattern(t.identifier('props'), t.objectExpression([]))], | |
t.blockStatement(body), | |
), | |
context, | |
); | |
return t.file(t.program([...imports, functionExport], [], 'module')); | |
} | |
function createFunctionExport(func, context) { | |
if (context.options.defaultExport) { | |
return t.exportDefaultDeclaration(func); | |
} | |
return t.exportNamedDeclaration(func); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment