Skip to content

Commit

Permalink
Generate React components for SVGs via script
Browse files Browse the repository at this point in the history
  • Loading branch information
pugnascotia committed Nov 22, 2017
1 parent f6aecea commit af96d87
Show file tree
Hide file tree
Showing 94 changed files with 3,246 additions and 984 deletions.
3 changes: 1 addition & 2 deletions .babelrc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"transform-async-generator-functions",
"transform-object-rest-spread",
// stage 2
"transform-class-properties",
"inline-react-svg"
"transform-class-properties"
]
}
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

When creating new components, adding new features, or fixing bugs, please refer to the [Component Development guidelines][docs-components]. If there isn't an associated issue on the bug tracker yet, consider creating one so that you get a chance to discuss the changes you have in mind with the rest of the team.

## SVG Icons

The SVG icons under `./src/components/icon/assets/` are transformed into React components during the build by `./scripts/compile-icons.js`. Run this manually if you add a new icons and need immediate access to the React component.

## Documentation

Always remember to update [documentation site][docs] and the [`CHANGELOG.md`](CHANGELOG.md) in the same PR that contains functional changes. We do this in tandem to prevent our examples from going out of sync with the actual components. In this sense, treat documentation no different than how you would treat tests.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@
"babel-jest": "21.0.0",
"babel-loader": "7.1.2",
"babel-plugin-add-module-exports": "0.2.1",
"babel-plugin-inline-react-svg": "^0.5.2",
"babel-plugin-transform-async-generator-functions": "6.24.1",
"babel-plugin-transform-class-properties": "6.24.1",
"babel-plugin-transform-object-rest-spread": "6.26.0",
"babel-preset-env": "1.6.1",
"babel-preset-react": "6.24.1",
"chalk": "^2.3.0",
"chokidar": "1.7.0",
"css-loader": "0.28.7",
"enzyme": "3.1.0",
Expand Down Expand Up @@ -85,7 +85,7 @@
"sass-loader": "6.0.6",
"sinon": "4.0.1",
"style-loader": "0.19.0",
"svg-sprite-loader": "3.3.1",
"svgo": "^1.0.3",
"webpack": "3.8.1",
"webpack-dev-server": "2.9.2",
"yeoman-generator": "2.0.1",
Expand Down
157 changes: 157 additions & 0 deletions scripts/compile-icons.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#!/usr/bin/env node
/* eslint-env node */

const fs = require('fs');
const path = require('path');
const SVGO = require('svgo');
const camelCase = require('lodash/camelCase');
const chalk = require('chalk');

const startTime = Date.now();

const svgo = new SVGO({
plugins: [
{ cleanupIDs: false },
{ removeViewBox: false }
]
});

// Each of these functions are applied to the SVG source in turn. I'd add svgo here too if it didn't return a Promise.
const replacements = [
ensureClosingTag,
insertTitle,
fixNamespaces,
fixDataAttributes,
fixSvgAttributes,
fixStyleAttributes
];

const svgs = fs.readdirSync('./src/components/icon/assets').filter(f => f.endsWith('.svg'));

for (const svg of svgs) {
const filepath = `./src/components/icon/assets/${svg}`;
const iconName = path.basename(svg, '.svg');

const rawSvgSource = fs.readFileSync(filepath, 'utf8');

svgo.optimize(rawSvgSource, { path: filepath }).then(function (result) {
const modifiedSvg = replacements.reduce((svg, eachFn) => eachFn(svg), result.data);

const { defaultProps, finalSvg } = extractDefaultProps(filepath, modifiedSvg);
const reactComponent = renderTemplate({
name: camelCase(iconName),
svg: finalSvg,
defaultProps
});

fs.writeFileSync(`./src/components/icon/iconComponents/${iconName}.js`, reactComponent);
});
}

console.log(chalk.green.bold(`✔ Finished generating React components from SVGs (${ Date.now() - startTime } ms)`));

/**
* Ensures that the root <svg> element is not self-closing. This is to generically handle empty.svg,
* whose source would otherwise break the other transforms.
* @param svg
*/
function ensureClosingTag(svg) {
return svg.replace(/\/>$/, '></svg>');
}

/**
* Adds a title element to the SVG, which svgo will have stripped out.
*/
function insertTitle(svg) {
const index = svg.indexOf('>') + 1;
return svg.slice(0, index) + '<title>{ title } </title>' + svg.slice(index);
}

/**
* Makes XML namespaces work with React.
*/
function fixNamespaces(svg) {
return svg.replace(/xmlns:xlink/g, 'xmlnsXlink').replace(/xlink:href/g, 'xlinkHref');
}

/**
* Camel-cases data attributes.
*/
function fixDataAttributes(svg) {
return svg.replace(/(data-[\w-]+)="([^"]+)"/g, (_, attr, value) => `${ camelCase(attr)}="${ value }"`);
}

/**
* Camel-cases SVG attributes.
*/
function fixSvgAttributes(svg) {
return svg.replace(/(fill-rule|stroke-linecap|stroke-linejoin)="([^"]+)"/g, (_, attr, value) => `${ camelCase(attr)}="${ value }"`);
}

/**
* Turns inline styles as strings into objects.
*/
function fixStyleAttributes(svg) {
return svg.replace(/(style)="([^"]+)"/g, (_, attr, value) => `style={${ JSON.stringify(cssToObj(value)) }}`);
}

/**
* Extracts the attributes on an SVG and returns them as default props for the React component,
* plus a modified SVG with those attributes stripped out. Also injects a props spread to that
* any other props passed to the React components are set on the SVG.
* @param {string} filename the name of the SVG
* @param {string} svg the SVG source
* @return {{defaultProps: {}, finalSvg: string}}
*/
function extractDefaultProps(filename, svg) {
const endIndex = svg.indexOf('>');
const attrString = svg.slice(4, endIndex).trim();

const defaultProps = {};

for (const pair of attrString.match(/(\w+)="([^"]+)"/g)) {
const [, name, value] = pair.match(/^(\w+)="([^"]+)"$/);
defaultProps[camelCase(name)] = value;
}

defaultProps.title = path.basename(filename, '.svg').replace(/_/g, ' ') + ' icon';

const finalSvg = '<svg { ...props }' + svg.slice(endIndex);

return {
defaultProps,
finalSvg
};
}

/**
* Generates the final React component.
*/
function renderTemplate({ name, svg, defaultProps }) {
return `// This is a generated file. Proceed with caution.
/* eslint-disable */
import React from 'react';
const ${name} = ({ title, ...props }) => ${ svg };
${name}.defaultProps = ${JSON.stringify(defaultProps, null, 2)};
export default ${name};
`;
}


/**
* Hack to convert a piece of CSS into an object
*/
function cssToObj(css) {
const o = {};
css.split(';').filter(el => !!el).forEach(el => {
const s = el.split(':');
const key = camelCase(s.shift().trim());
const value = s.join(':').trim();
o[key] = value;
});

return o;
}
1 change: 1 addition & 0 deletions scripts/compile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ set -e

./scripts/compile-clean.sh
./scripts/compile-scss.sh
./scripts/compile-icons.js
./scripts/compile-eui.sh
8 changes: 1 addition & 7 deletions src-docs/webpack.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const path = require('path');
const HtmlWebpackPlugin = require(`html-webpack-plugin`);
const SpriteLoaderPlugin = require('svg-sprite-loader/plugin');

module.exports = {
devtool: 'source-map',
Expand Down Expand Up @@ -29,9 +28,6 @@ module.exports = {
test: /\.css$/,
loaders: ['style-loader/useable', 'css-loader'],
exclude: /node_modules/
}, {
test: /\.svg$/,
loader: 'svg-sprite-loader'
}, {
test: /\.(woff|woff2|ttf|eot|ico)(\?|$)/,
loader: 'file-loader',
Expand All @@ -45,9 +41,7 @@ module.exports = {
inject: 'body',
cache: true,
showErrors: true
}),

new SpriteLoaderPlugin()
})
],

devServer: {
Expand Down
17 changes: 15 additions & 2 deletions src/components/accordion/__snapshots__/accordion.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,25 @@ exports[`EuiAccordion is rendered 1`] = `
>
<svg
class="euiIcon euiIcon--medium"
height="16"
viewBox="0 0 16 16"
width="16"
xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
>
<title>
arrow right icon
arrow right icon
</title>
<defs>
<path
d="M13.069 5.157L8.384 9.768a.546.546 0 0 1-.768 0L2.93 5.158a.552.552 0 0 0-.771 0 .53.53 0 0 0 0 .759l4.684 4.61c.641.631 1.672.63 2.312 0l4.684-4.61a.53.53 0 0 0 0-.76.552.552 0 0 0-.771 0z"
id="arrow_right-a"
/>
</defs>
<use
href="#arrow_right"
fill-rule="nonzero"
href="#arrow_right-a"
transform="matrix(0 1 1 0 0 0)"
/>
</svg>
</div>
Expand Down
Loading

0 comments on commit af96d87

Please sign in to comment.