Skip to content

Commit

Permalink
First version
Browse files Browse the repository at this point in the history
  • Loading branch information
Jordan Gensler committed Sep 19, 2016
1 parent 15e011a commit fea1021
Show file tree
Hide file tree
Showing 13 changed files with 288 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["airbnb"]
}
4 changes: 4 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "airbnb",
"root": true
}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
lib
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# babel-plugin-inline-react-svg

Inlines requires to SVG files so that they can be used seamlessly in your components.

Inspired by and code foundation provided by [react-svg-loader](https://github.com/boopathi/react-svg-loader).
48 changes: 48 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "babel-plugin-inline-react-svg",
"version": "0.1.0",
"description": "A babel plugin that optimizes and inlines SVGs for your react components.",
"main": "lib/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"sanity": "babel-node test/sanity.js",
"build": "babel src --out-dir lib",
"lint": "eslint src/",
"prepublish": "npm run build"
},
"repository": {
"type": "git",
"url": "git+https://github.com/kesne/babel-plugin-inline-react-svg.git"
},
"keywords": [
"babel",
"plugin",
"react",
"svg",
"inline"
],
"author": "Jordan Gensler <jordan.gensler@airbnb.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/kesne/babel-plugin-inline-react-svg/issues"
},
"homepage": "https://github.com/kesne/babel-plugin-inline-react-svg#readme",
"devDependencies": {
"babel-cli": "^6.14.0",
"babel-core": "^6.14.0",
"babel-preset-airbnb": "^2.0.0",
"eslint": "^3.5.0",
"eslint-config-airbnb": "^11.1.0",
"eslint-plugin-import": "^1.15.0",
"eslint-plugin-jsx-a11y": "^2.2.2",
"eslint-plugin-react": "^6.2.2",
"react": "^15.3.1"
},
"dependencies": {
"babel-template": "^6.15.0",
"babel-traverse": "^6.15.0",
"babylon": "^6.10.0",
"lodash.isplainobject": "^4.0.6",
"svgo": "^0.7.0"
}
}
7 changes: 7 additions & 0 deletions src/camelize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function hyphenToCamel(name) {
return name.replace(/-([a-z])/g, g => g[1].toUpperCase());
}

export function namespaceToCamel(namespace, name) {
return namespace + name.charAt(0).toUpperCase() + name.slice(1);
}
13 changes: 13 additions & 0 deletions src/cssToObj.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function cssToObj(css) {
const o = {};
css.split(';')
.filter(el => !!el)
.forEach((el) => {
const s = el.split(':');
const key = s.shift().trim();
const value = s.join(':').trim();
o[key] = value;
});

return o;
}
44 changes: 44 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { extname, dirname, join } from 'path';
import { readFileSync } from 'fs';
import template from 'babel-template';
import traverse from 'babel-traverse';
import { parse } from 'babylon';
import optimize from './optimize';
import transformSvg from './transformSvg';

const buildSvg = template(`
var SVG_NAME = function SVG_NAME(props) { return SVG_CODE; };
`);

export default ({ types: t }) => ({
visitor: {
ImportDeclaration(path, state) {
// This plugin only applies for SVGs:
if (extname(path.node.source.value) === '.svg') {
// We only support the import default specifier, so let's use that identifier:
const importIdentifier = path.node.specifiers[0].local;
const iconPath = state.file.opts.filename;
const absoluteIconPath = join(__dirname, '..', iconPath);
const svgPath = join(dirname(absoluteIconPath), path.node.source.value);
const svgSource = readFileSync(svgPath, 'utf8');
const optimizedSvgSource = optimize(svgSource);

const parsedSvgAst = parse(optimizedSvgSource, {
sourceType: 'module',
plugins: ['jsx'],
});

traverse(parsedSvgAst, transformSvg(t));

const svgCode = traverse.removeProperties(parsedSvgAst.program.body[0].expression);

const svgReplacement = buildSvg({
SVG_NAME: importIdentifier,
SVG_CODE: svgCode,
});

path.replaceWith(svgReplacement);
}
},
},
});
72 changes: 72 additions & 0 deletions src/optimize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// validates svgo opts
// to contain minimal set of plugins that will strip some stuff
// for the babylon JSX parser to work
import Svgo from 'svgo';
import isPlainObject from 'lodash.isplainobject';

const SVGO_OPTIONS = {};

const essentialPlugins = ['removeDoctype', 'removeComments'];

function isEssentialPlugin(p) {
return essentialPlugins.indexOf(p) !== -1;
}

function validateAndFix(opts) {
if (!isPlainObject(opts)) return;

if (opts.full) {
if (
typeof opts.plugins === 'undefined' ||
(Array.isArray(opts.plugins) && opts.plugins.length === 0)
) {
opts.plugins = [...essentialPlugins];
return;
}
}

// opts.full is false, plugins can be empty
if (typeof opts.plugins === 'undefined') return;
if (Array.isArray(opts.plugins) && opts.plugins.length === 0) return;

// track whether its defined in opts.plugins
var state = essentialPlugins.reduce((p, c) => Object.assign(p, {[c]: false}), {});

opts.plugins.map(p => {
if (typeof p === 'string' && isEssentialPlugin(p)) {
state[p] = true;
} else if (typeof p === 'object') {
Object.keys(p).forEach(k => {
if (isEssentialPlugin(k)) {
// make it essential
if (!p[k]) p[k] = true;
// and update state
state[k] = true;
}
});
}
});

Object.keys(state)
.filter(key => !state[key])
.forEach(key => opts.plugins.push(key));
}

module.exports = function optimize(content) {
validateAndFix(SVGO_OPTIONS);
const svgo = new Svgo(SVGO_OPTIONS);

// Babel needs to be sync, so we fake it:
var returnValue;
svgo.optimize(content, (response) => {
if (response.error) {
returnValue = response.error;
} else {
returnValue = response.data;
}
});


while (!returnValue) {}
return returnValue;
}
66 changes: 66 additions & 0 deletions src/transformSvg.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable no-param-reassign */
//
// These visitors normalize the SVG into something React understands:
//

import { namespaceToCamel, hyphenToCamel } from './camelize';
import cssToObj from './cssToObj';

export default t => ({
JSXAttribute(path) {
if (t.isJSXNamespacedName(path.node.name)) {
// converts
// <svg xmlns:xlink="asdf">
// to
// <svg xmlnsXlink="asdf">
path.node.name = t.jSXIdentifier(
namespaceToCamel(path.node.name.namespace.name, path.node.name.name.name)
);
} else if (t.isJSXIdentifier(path.node.name)) {
// converts
// <tag class="blah blah1"/>
// to
// <tag className="blah blah1"/>
if (path.node.name.name === 'class') {
path.node.name.name = 'className';
}

// converts
// <tag style="text-align: center; width: 50px">
// to
// <tag style={{textAlign: 'center', width: '50px'}}>
if (path.node.name.name === 'style') {
const csso = cssToObj(path.node.value.value);
const properties = Object.keys(csso).map(prop => t.objectProperty(
t.identifier(hyphenToCamel(prop)),
t.stringLiteral(csso[prop])
));
path.node.value = t.jSXExpressionContainer(
t.objectExpression(properties)
);
}

// converts
// <svg stroke-width="5">
// to
// <svg strokeWidth="5">
path.node.name.name = hyphenToCamel(path.node.name.name);
}
},

// converts
// <svg>
// to
// <svg {...props}>
// after passing through attributes visitors
JSXOpeningElement(path) {
if (path.node.name.name.toLowerCase() === 'svg') {
// add spread props
path.node.attributes.push(
t.jSXSpreadAttribute(
t.identifier('props')
)
);
}
},
});
2 changes: 2 additions & 0 deletions test/fixtures/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions test/fixtures/test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import MySvg from './close.svg';

export function MyFunctionIcon() {
return <MySvg />;
}

export class MyClassIcon extends React.Component {
render() {
return <MySvg />;
}
}
11 changes: 11 additions & 0 deletions test/sanity.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { transformFile } from 'babel-core';

transformFile('test/fixtures/test.jsx', {
presets: ['airbnb'],
plugins: [
'../../src/index',
],
}, (err, result) => {
if (err) throw err;
console.log(result.code);
});

0 comments on commit fea1021

Please sign in to comment.