Skip to content

Commit af96d87

Browse files
committed
Generate React components for SVGs via script
1 parent f6aecea commit af96d87

File tree

94 files changed

+3246
-984
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+3246
-984
lines changed

.babelrc

+1-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
"transform-async-generator-functions",
1919
"transform-object-rest-spread",
2020
// stage 2
21-
"transform-class-properties",
22-
"inline-react-svg"
21+
"transform-class-properties"
2322
]
2423
}

CONTRIBUTING.md

+4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
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.
88

9+
## SVG Icons
10+
11+
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.
12+
913
## Documentation
1014

1115
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.

package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@
4343
"babel-jest": "21.0.0",
4444
"babel-loader": "7.1.2",
4545
"babel-plugin-add-module-exports": "0.2.1",
46-
"babel-plugin-inline-react-svg": "^0.5.2",
4746
"babel-plugin-transform-async-generator-functions": "6.24.1",
4847
"babel-plugin-transform-class-properties": "6.24.1",
4948
"babel-plugin-transform-object-rest-spread": "6.26.0",
5049
"babel-preset-env": "1.6.1",
5150
"babel-preset-react": "6.24.1",
51+
"chalk": "^2.3.0",
5252
"chokidar": "1.7.0",
5353
"css-loader": "0.28.7",
5454
"enzyme": "3.1.0",
@@ -85,7 +85,7 @@
8585
"sass-loader": "6.0.6",
8686
"sinon": "4.0.1",
8787
"style-loader": "0.19.0",
88-
"svg-sprite-loader": "3.3.1",
88+
"svgo": "^1.0.3",
8989
"webpack": "3.8.1",
9090
"webpack-dev-server": "2.9.2",
9191
"yeoman-generator": "2.0.1",

scripts/compile-icons.js

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
#!/usr/bin/env node
2+
/* eslint-env node */
3+
4+
const fs = require('fs');
5+
const path = require('path');
6+
const SVGO = require('svgo');
7+
const camelCase = require('lodash/camelCase');
8+
const chalk = require('chalk');
9+
10+
const startTime = Date.now();
11+
12+
const svgo = new SVGO({
13+
plugins: [
14+
{ cleanupIDs: false },
15+
{ removeViewBox: false }
16+
]
17+
});
18+
19+
// 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.
20+
const replacements = [
21+
ensureClosingTag,
22+
insertTitle,
23+
fixNamespaces,
24+
fixDataAttributes,
25+
fixSvgAttributes,
26+
fixStyleAttributes
27+
];
28+
29+
const svgs = fs.readdirSync('./src/components/icon/assets').filter(f => f.endsWith('.svg'));
30+
31+
for (const svg of svgs) {
32+
const filepath = `./src/components/icon/assets/${svg}`;
33+
const iconName = path.basename(svg, '.svg');
34+
35+
const rawSvgSource = fs.readFileSync(filepath, 'utf8');
36+
37+
svgo.optimize(rawSvgSource, { path: filepath }).then(function (result) {
38+
const modifiedSvg = replacements.reduce((svg, eachFn) => eachFn(svg), result.data);
39+
40+
const { defaultProps, finalSvg } = extractDefaultProps(filepath, modifiedSvg);
41+
const reactComponent = renderTemplate({
42+
name: camelCase(iconName),
43+
svg: finalSvg,
44+
defaultProps
45+
});
46+
47+
fs.writeFileSync(`./src/components/icon/iconComponents/${iconName}.js`, reactComponent);
48+
});
49+
}
50+
51+
console.log(chalk.green.bold(`✔ Finished generating React components from SVGs (${ Date.now() - startTime } ms)`));
52+
53+
/**
54+
* Ensures that the root <svg> element is not self-closing. This is to generically handle empty.svg,
55+
* whose source would otherwise break the other transforms.
56+
* @param svg
57+
*/
58+
function ensureClosingTag(svg) {
59+
return svg.replace(/\/>$/, '></svg>');
60+
}
61+
62+
/**
63+
* Adds a title element to the SVG, which svgo will have stripped out.
64+
*/
65+
function insertTitle(svg) {
66+
const index = svg.indexOf('>') + 1;
67+
return svg.slice(0, index) + '<title>{ title } </title>' + svg.slice(index);
68+
}
69+
70+
/**
71+
* Makes XML namespaces work with React.
72+
*/
73+
function fixNamespaces(svg) {
74+
return svg.replace(/xmlns:xlink/g, 'xmlnsXlink').replace(/xlink:href/g, 'xlinkHref');
75+
}
76+
77+
/**
78+
* Camel-cases data attributes.
79+
*/
80+
function fixDataAttributes(svg) {
81+
return svg.replace(/(data-[\w-]+)="([^"]+)"/g, (_, attr, value) => `${ camelCase(attr)}="${ value }"`);
82+
}
83+
84+
/**
85+
* Camel-cases SVG attributes.
86+
*/
87+
function fixSvgAttributes(svg) {
88+
return svg.replace(/(fill-rule|stroke-linecap|stroke-linejoin)="([^"]+)"/g, (_, attr, value) => `${ camelCase(attr)}="${ value }"`);
89+
}
90+
91+
/**
92+
* Turns inline styles as strings into objects.
93+
*/
94+
function fixStyleAttributes(svg) {
95+
return svg.replace(/(style)="([^"]+)"/g, (_, attr, value) => `style={${ JSON.stringify(cssToObj(value)) }}`);
96+
}
97+
98+
/**
99+
* Extracts the attributes on an SVG and returns them as default props for the React component,
100+
* plus a modified SVG with those attributes stripped out. Also injects a props spread to that
101+
* any other props passed to the React components are set on the SVG.
102+
* @param {string} filename the name of the SVG
103+
* @param {string} svg the SVG source
104+
* @return {{defaultProps: {}, finalSvg: string}}
105+
*/
106+
function extractDefaultProps(filename, svg) {
107+
const endIndex = svg.indexOf('>');
108+
const attrString = svg.slice(4, endIndex).trim();
109+
110+
const defaultProps = {};
111+
112+
for (const pair of attrString.match(/(\w+)="([^"]+)"/g)) {
113+
const [, name, value] = pair.match(/^(\w+)="([^"]+)"$/);
114+
defaultProps[camelCase(name)] = value;
115+
}
116+
117+
defaultProps.title = path.basename(filename, '.svg').replace(/_/g, ' ') + ' icon';
118+
119+
const finalSvg = '<svg { ...props }' + svg.slice(endIndex);
120+
121+
return {
122+
defaultProps,
123+
finalSvg
124+
};
125+
}
126+
127+
/**
128+
* Generates the final React component.
129+
*/
130+
function renderTemplate({ name, svg, defaultProps }) {
131+
return `// This is a generated file. Proceed with caution.
132+
/* eslint-disable */
133+
import React from 'react';
134+
135+
const ${name} = ({ title, ...props }) => ${ svg };
136+
137+
${name}.defaultProps = ${JSON.stringify(defaultProps, null, 2)};
138+
139+
export default ${name};
140+
`;
141+
}
142+
143+
144+
/**
145+
* Hack to convert a piece of CSS into an object
146+
*/
147+
function cssToObj(css) {
148+
const o = {};
149+
css.split(';').filter(el => !!el).forEach(el => {
150+
const s = el.split(':');
151+
const key = camelCase(s.shift().trim());
152+
const value = s.join(':').trim();
153+
o[key] = value;
154+
});
155+
156+
return o;
157+
}

scripts/compile.sh

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ set -e
44

55
./scripts/compile-clean.sh
66
./scripts/compile-scss.sh
7+
./scripts/compile-icons.js
78
./scripts/compile-eui.sh

src-docs/webpack.config.js

+1-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
const path = require('path');
22
const HtmlWebpackPlugin = require(`html-webpack-plugin`);
3-
const SpriteLoaderPlugin = require('svg-sprite-loader/plugin');
43

54
module.exports = {
65
devtool: 'source-map',
@@ -29,9 +28,6 @@ module.exports = {
2928
test: /\.css$/,
3029
loaders: ['style-loader/useable', 'css-loader'],
3130
exclude: /node_modules/
32-
}, {
33-
test: /\.svg$/,
34-
loader: 'svg-sprite-loader'
3531
}, {
3632
test: /\.(woff|woff2|ttf|eot|ico)(\?|$)/,
3733
loader: 'file-loader',
@@ -45,9 +41,7 @@ module.exports = {
4541
inject: 'body',
4642
cache: true,
4743
showErrors: true
48-
}),
49-
50-
new SpriteLoaderPlugin()
44+
})
5145
],
5246

5347
devServer: {

src/components/accordion/__snapshots__/accordion.test.js.snap

+15-2
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,25 @@ exports[`EuiAccordion is rendered 1`] = `
2525
>
2626
<svg
2727
class="euiIcon euiIcon--medium"
28+
height="16"
29+
viewBox="0 0 16 16"
30+
width="16"
31+
xlink="http://www.w3.org/1999/xlink"
32+
xmlns="http://www.w3.org/2000/svg"
2833
>
2934
<title>
30-
arrow right icon
35+
arrow right icon
3136
</title>
37+
<defs>
38+
<path
39+
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"
40+
id="arrow_right-a"
41+
/>
42+
</defs>
3243
<use
33-
href="#arrow_right"
44+
fill-rule="nonzero"
45+
href="#arrow_right-a"
46+
transform="matrix(0 1 1 0 0 0)"
3447
/>
3548
</svg>
3649
</div>

0 commit comments

Comments
 (0)