-
Notifications
You must be signed in to change notification settings - Fork 843
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Generate React components for SVGs via script
- Loading branch information
1 parent
f6aecea
commit af96d87
Showing
94 changed files
with
3,246 additions
and
984 deletions.
There are no files selected for viewing
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
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
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
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
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; | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,5 @@ set -e | |
|
||
./scripts/compile-clean.sh | ||
./scripts/compile-scss.sh | ||
./scripts/compile-icons.js | ||
./scripts/compile-eui.sh |
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
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
Oops, something went wrong.