Skip to content

Instantly share code, notes, and snippets.

@sergeysova
Last active July 28, 2022 15:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sergeysova/b6acdb75fc4a97aec6d451687f76955f to your computer and use it in GitHub Desktop.
Save sergeysova/b6acdb75fc4a97aec6d451687f76955f to your computer and use it in GitHub Desktop.
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