Add experimental Webpack support. Works fine when no cache is involved, but I need to figure out a better story for cache integration as most builds that teams use likely include a cache enabled.

This commit is contained in:
Ryan McGrath 2016-03-21 19:39:16 +09:00
parent f2040233d0
commit 3801044178
18 changed files with 40032 additions and 486 deletions

View file

@ -6,10 +6,20 @@ This project provides a way to utilize SVG icons in React-based projects with ea
- A **Browserify plugin** that handles the behind-the-scenes aspects of creating your icon library as a require()-able JS module and injecting it into your bundle. This plugin also handles injecting a lightweight React Component which can be utilized to reference graphics.
- A **Webpack plugin** that handles the behind-the-scenes aspects of creating your icon library and injecting it into your bundle. You get the same aforementioned React Icon component that's referenced above, too.
- The aforementioned **React Component**, which transparently handles mapping between SVG and IMG tags in various browsers. It's optimized to be fast, and comes with no external dependencies beyond React itself.
- _**Bonus:**_ react-iconpack ships with **over 1,000 ready to use SVG icons** from various different icon projects. Hit the ground running and avoid the tedious task of gathering all the assets you need.
Note
==============
The files in this repository are considered bleeding-edge at the moment. The version currently available on `npm` uses the instructions below; the new version (when released) will feature the Webpack plugin and potentially drop support for generating PNG assets.
The Webpack plugin currently works, but falls apart when caching builds (either via Webpack or Babel) is turned on. This is because I'm currently investigating the best way to integrate with existing cache solutions instead of rolling my own. If you'd like to help out, clone this repository and install the `devDependencies`, then check out the `webpack-test` folder. The `webpack.config.js` includes an example setup for Webpack integration.
In terms of caching, the only issue is that the Babel plugin can't be re-run on source code that's cached as already built. Where/how to store accrued SVG nodes becomes complex, and slightly dependent on the upstream cache. Would love to hear ideas from others!
Installation and Usage
==============
react-iconpack is currently delivered via npm; if there's some other module vendor system that people want to utilize open a pull request and I'll take a look at it.

19351
browserify-test/dist/build.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>react-iconpack Browserify Test</title>
</head>
<body>
<div id="test-node"></div>
<script src="dist/build.js" type="text/javascript"></script>
</body>
</html>

View file

@ -0,0 +1,13 @@
import React from 'react';
import {render} from 'react-dom';
import Icon from 'react-iconpack';
class TestComponent extends React.Component {
render() {
return <Icon uri="polymer/notification/disc_full" width="48" height="48"/>;
}
};
render(<TestComponent />, document.getElementById('test-node'));

1
components/react-iconpack-icons.js vendored Normal file
View file

@ -0,0 +1 @@
module.exports = {react_iconpack_icons: {}};

View file

@ -1,21 +1,20 @@
/**
* This is a React JSX tag that will take care of managing injecting the
* icons properly after they've been compiled into your bundle. As long as you're
* using the Browserify plugin this will "just work" - if you'd like to see plugins
* using the Browserify or Webpack plugin this will "just work" - if you'd like to see plugins
* for other platforms, submit a pull request!
*
* Note that this file is liberally commented; React is pretty intuitive but I prefer
* if people can easily decipher what I'm trying to accomplish and learn from it. You
* may have a different style, but try to follow it here if you modify this.
*
* @author Ryan McGrath <ryan@venodesigns.net>, contributors (see repo)
* @author Ryan McGrath <ryan@rymc.io>, contributors (see repo)
* @license MIT
* @repo https://github.com/ryanmcgrath/react-iconpack/
*/
var React = require('react');
icons = require('react-iconpack-icons');
var icons = require('react-iconpack-icons');
/**
* A very basic and simple wrapper around console.warn, for debug purposes.
@ -29,8 +28,7 @@ var warn = function(msg) {
/**
* The main JSX tag you'll be wanting. Depending on which mode your bundle is
* in, it will handle injecting either an <svg> tag or an <img> tag. For any global
* The main JSX tag you'll be wanting. For any global
* styling needs you can safely target the "react-iconpack-icon" class in CSS - you
* can also add your own, of course, but there's one there for convenience.
*
@ -53,8 +51,7 @@ module.exports = React.createClass({
title: '',
desc: '',
// These are only used for SVGs as the Browser will do magic by default
// for PNGs. Experienced SVG users can override this as necessary but I'm
// Experienced SVG users can override this as necessary but I'm
// interested in providing an easier solution, not the slight headache that
// SVG can be.
//
@ -119,10 +116,10 @@ module.exports = React.createClass({
*/
shouldComponentUpdate: function(nextProps, nextState) {
var p = this.props,
np = nextProps, // lol
np = nextProps,
k;
if(this.props === nextProps) // Shallow reference
if(this.props === np)
return false;
for(k in p)
@ -137,67 +134,12 @@ module.exports = React.createClass({
},
/**
* This needs no documentation, it's a render method.
*
* @returns {Object} A JSX "SVG" tag configured based on the properties assigned.
*/
render: function() {
if(icons.mode === 'png')
return this.renderAsPNG();
else
return this.renderAsSVG();
},
/**
* This seems a little heavy to do here, yes, but I think it's the right
* way to go about it - if anyone has better ideas please feel free to open
* an issue or submit a pull request.
*
* Basically, for an <img> tag we need to shuffle some attributes around. We
* also want to make this accessible, if possible - ideally by moving over the
* accessibility attributes from the typical <svg> approach to here. We also
* need to move the uri attribute, and considering it's a base64 PNG I don't
* feel like arbitrarily copying the data to the state and duplicating it.
*
* In reality this might be overkill, but hey, it works fine in my opinion.
* Pull request it if it bothers you. We optimize in shouldComponentUpdate
* anyway to make it so this hopefully won't be re-hit too much.
*
* @returns {Object} A JSX Img tag with SVG attributes shuffled to match.
*/
renderAsPNG: function() {
var props = {
src: ['data:image/png;base64,', icons.icons[this.props.uri]].join(''),
alt: this.props.title + ': ' + this.props.desc,
'aria-labeledby': 'alt'
};
// We don't want to copy these over. This is also rather naive; there's little
// reason anyone should be passing Objects or Arrays or whatever the kids are
// into these days into this tag, but if they do, it has the potential to get
// stupid.
for(var prop in this.props) {
if(
prop === 'src' || prop === 'title' ||
prop === 'desc' || prop === 'aria-labeledby'
) continue;
if(prop === 'className')
props.className = 'react-iconpack-icon ' + this.props.className;
props[prop] = this.props[prop];
}
return React.createElement('img', React.__spread({}, props));
},
/**
* This is, in comparison, much more straightforward. We provide some default stuff
* We provide some default stuff
* that makes working with an SVG tag a bit more like working with an IMG tag.
*
* @returns {Object} A JSX SVG tag configured and what not.
*/
renderAsSVG: function() {
render: function() {
var path,
accessibility,
icon,

31
gulpfile.js Normal file
View file

@ -0,0 +1,31 @@
var gulp = require('gulp'),
fs = require('fs'),
browserify = require('browserify'),
iconpacker = require('react-iconpack')('browserify');
var compile = function() {
var bundler = browserify('./browserify-test/src/index.js', {
debug: true,
});
iconpacker.attachBrowserifyInjector(bundler, {
verbose: true
});
bundler.require('react').require('react-dom').transform('babelify', {
presets: ['es2015', 'react'],
plugins: ['react-iconpack']
});
bundler.bundle()
.on('error', function(err) {
console.error('Failure! ' + err);
this.emit('end');
})
.pipe(fs.createWriteStream('./browserify-test/dist/build.js'));
};
gulp.task('compile', compile);
gulp.task('default', ['compile']);

429
index.js
View file

@ -2,403 +2,78 @@
* react-iconpack is a library to make automating SVG/PNG icon usage in React
* a bit more streamlined. It consists of a few key components:
*
* - A Babel transformer/plugin that determines which SVG
* - A Babel (6) transformer/plugin that determines which SVG
* files you actually want to load, straight from your
* source.
*
* - An easy to use <Icon.../> React JSX tag that ties in with
* this library.
*
* - A Browserify plugin that will grab your SVG files, optimize
* - A Webpack plugin that will grab your SVG files, optimize
* them, and auto-inject them into your bundle as a require()-able
* module. The <Icon.../> tag will also transparently handle adding
* things to the browser document for you.
*
* - Bonus: For browsers that don't support PNG (e.g, IE8) you can
* instruct IconPacker to build a module with base64'd PNGs.
*
* Found a bug, use Webpack, need binary pngs instead of base64?
* Send a pull request. :)
* Found a bug? Send a pull request. :)
*/
var SVGO = require('svgo'),
fs = require('fs'),
async = require('async'),
through = require('through2'),
merge = require('lodash/object/merge'),
ReactSVGComponentFilePath = require.resolve('./components/svg.js'),
im, pngquant;
// Optional dependencies
try {
pngquant = require('node-pngquant-native');
im = require('gm').subClass({
imageMagick: true
});
} catch(e) {
// Nothing to do here
}
var IconPacker = require('./lib/IconPacker'),
BabelSVGTracker = require('./lib/BabelSVGTracker'),
attachBrowserifyInjector = require('./lib/attachBrowserifyInjector'),
WebpackPlugin = require('./lib/WebpackPlugin'),
packer;
/**
* IconPackerer constructor.
* This thing responds differently depending on the environment.
*
* @constructor
* @param {Object} options Options
* @return {Object} An instance of IconPacker to do things with.
*/
var IconPacker = function(options) {
if(!(this instanceof IconPacker))
return new IconPacker(options);
// This is a reference to the directory where we've got
// provided SVG files held. The user can specify a root directory of
// their own in the options below; we'll always check theirs first, falling
// back to checking ours if there's no hit.
this._internalSVGDirectory = __dirname + '/svgs/';
this.opts = merge({
verbose: false,
mode: 'svg',
// JSX tags to pick up
JSXTagNames: ['Icon'],
injectReactComponent: true,
iconLibraryNamespace: 'react-iconpack-icons',
// We provide some default SVGO options that work well for the icons in this
// project; users can override but keep in mind that it's ill-advised at the time
// of writing this to remove the viewBox attributes. This may change in a later release.
//
// (Removing the viewBox can cause very wonky issues in IE, and it helps to size the various icons).
svgSourceDirectory: '',
svgo: {
plugins: [
{removeViewBox: false}, // See note above ---^
{removeUselessStrokeAndFill: false}, // This can clobber some icons, just leave it.
{removeEmptyAttrs: false}, // I find this helpful, others may not.
{removeDimensions: true}
]
},
png: {
antialias: true,
density: 1000,
width: 32,
quality: 90,
compressorQuality: [60, 80],
background: 'rgba(0,0,0,0)',
speed: 3,
ie6fix: false
}
}, options);
// This is our grand database of SVGs. A great @TODO here would
// be some caching, but I've yet to hit speed limitations where I've
// really needed it. Pull requests welcome!
this.SVGS = {};
// Basic binding() and such for pieces we use later on in the lifepsan
// of this object. See the respective methods for more details.
this.svgo = new SVGO(this.opts.svgo);
this.JSXSVGTracker = this._SVGTracker.bind(this);
this.BrowserifyInjector = this._BrowserifyInjector.bind(this);
this.readAndOptimizeSVGs = this._readAndOptimizeSVGs.bind(this);
// PNG mode might not be needed or desired by some people so I don't see a reason
// to make them screw around with extra dependencies. For those who opt into it,
// let's check and ensure that they've got the required modules installed.
//
// gm not being around is an error; pngquant is a nice-to-have but we can technically
// run without it.
if(this.opts.mode === 'png') {
if(!im)
throw new Error(
'PNG mode, but no trace of Node gm.\n' +
'You probably want to run:\n\n"npm install gm"\n\n and make sure ' +
'that ImageMagick is installed with librsvg support. On a Mac ' +
'with Homebrew, this would be something like:\n\n' +
'brew install imagemagick --with-librsvg\n\n'
);
if(!pngquant)
console.warn(
'PNG mode, but no trace of pngquant. Compilation will continue ' +
'but PNGs may be un-optimized. To fix this you probably want to run:' +
'\n\nnpm install node-pngquant-native\n\n'
)
}
return this;
};
/**
* Handles async reading in all the SVG files and optimizing them. Really
* nothing too special here - just async.map'd later on down the file.
* - If the engine is "webpack", it returns a function to build a new
* Webpack plugin/IconPacker combo in a way that syntactically makes sense
* in a webpack.config.js scenario.
*
* @param {Function} callback Upon completion this junks runs.
*/
IconPacker.prototype._readAndOptimizeSVGs = function(key, callback) {
var packer = this,
internalFilePath = this._internalSVGDirectory + key + '.svg',
filePath = packer.opts.svgSourceDirectory + key + '.svg';
var onSVGOComplete = function(result) {
packer.SVGS[key] = result;
callback(null, result);
};
fs.readFile(filePath, 'utf8', function(error, data) {
if(!error)
return packer.svgo.optimize(data, onSVGOComplete);
fs.readFile(internalFilePath, 'utf8', function(e, d) {
if(!e)
return packer.svgo.optimize(d, onSVGOComplete);
console.warn('Warning: Could not load ' + key);
return callback(e, null);
});
});
};
/**
* A Babel "plugin/transformer" that just tracks unique <Icon.../>
* JSX tags in your source code. Note that this does absolutely no
* modifications - just accumulates the unique SVG uris for the mapping.
* - If the engine is "browserify", then it returns a function to attach a
* handler to a Browserify bundler automagically.
*
* @param {Object} babel This will be auto-passed, most likely, by Babel.
* @returns {Object} babel.Transformer used for the babel'ing. You know the one.
* - If .types exists, then it's (most likely) babel trying to load it - babel.types
* is the standard API for babel plugins. We return an <Icon> tracker plugin in this
* case.
*
* - If no engine is specified, then it returns a Webpack loader. We do this to handle
* actually injecting the icons into the source code, because the Webpack API is really
* not clear on how to extend it like this.
*
* The reason for this scenario is that in a Webpack plugin it's nigh-impossible to
* specify anything other than a simple package import for a plugin (insofar as I can tell).
* This method allows configuration to occur in a way that's easy for people to reason about:
* in either Browserify or Webpack, just add "react-iconpack" to the "plugins" list, and then
* use the appropriate plugin itself and everything happens automatically behind the scenes.
*
* This all relies on how modules work behind the scenes - once called, they're cached and
* return the same stuff on repeated require() calls. This allows us to share 1 packer between
* a Webpack/Browserify plugin and the Babel SVG <Icon> tracker. The tracker accumulates calls, the
* plugins inject the necessary module code.
*
* @param {String} engine The engine to use, or nothing if you want the Babel plugin.
* @returns {Object} engine A different object depending on how this was called. See above.
*/
IconPacker.prototype._SVGTracker = function(babel) {
var packer = this;
module.exports = function(engine) {
if(typeof packer === 'undefined')
packer = new IconPacker({});
return new babel.Transformer('react-iconpack', {
JSXElement: function JSXElement(node, parent, scope, file) {
if(packer.opts.JSXTagNames.indexOf(node.openingElement.name.name) < 0)
return;
var attributes = node.openingElement.attributes,
l = attributes.length,
i = 0;
for(; i < l; i++) {
if(attributes[i].name.name !== 'uri')
continue;
packer.SVGS[attributes[i].value.value] = 1;
}
}
});
};
/**
* A Browserify plugin that hooks in to the Browserify build pipeline and injects
* your icons and a tag as a require()-able module.
*
* If you use another module loader or something I'm sure you can probably figure
* it out.
*
* Originally I wanted to find a way to just have Babel shove this in, but I
* couldn't figure out a way to do it cleanly so this works. If you use Webpack
* and would like to see a plugin like this, pull requests are welcome.
*
* Note: you almost never need to call this yourself; you really just wanna pass
* it to Browserify instead. See the full documentation for more details.
*
* @param {Object} browserify An instance of Browserify for this to hook into.
* @param {String} imgType Either "png" or "svg".
* @returns {Object} browserify The instance being operated on.
*/
IconPacker.prototype._BrowserifyInjector = function(browserify, opts) {
var startListeningToThisCompleteMessOfALibraryAgain = function() {
browserify.pipeline.get('pack').unshift(this.compile());
}.bind(this);
browserify.external('react-iconpack');
browserify.external('react-iconpack-icons');
browserify.on('reset', startListeningToThisCompleteMessOfALibraryAgain);
startListeningToThisCompleteMessOfALibraryAgain();
return browserify;
};
/**
* Handles actually compiling the SVG (or PNG) assets into a module, then passing
* it into the build stream for inclusion in the final bundle. I'll note that this
* was a massive PITA to decipher how to do - comments are your friends, Node devs.
*
* This is the only method that is "callback-hell"-ish, but I've chosen not to break
* it up because it still fits on one screen of code and is follow-able enough.
*
* @returns {Function} A through2 stream in object mode, which Browserify needs.
*/
IconPacker.prototype.compile = function() {
var packer = this,
write = function(buf, enc, next) {
next(null, buf);
if(engine === 'webpack') {
return function(opts) {
return new WebpackPlugin(packer, opts);
};
var end = function(next) {
var keys = Object.keys(packer.SVGS),
stream = this;
async.map(keys, packer.readAndOptimizeSVGs, function(error, results) {
fs.readFile(ReactSVGComponentFilePath, 'utf8', function(e, data) {
stream.push({
id: 'react-iconpack',
externalRequireName: 'react-iconpack',
standaloneModule: 'react-iconpack',
hasExports: true,
source: data,
// An empty deps object is important here as we don't want to
// accidentally bundle a second copy of React or the icons as
// they're require()'d in the source. Don't change this.
deps: {}
});
var icons = {
id: 'react-iconpack-icons',
externalRequireName: 'react-iconpack-icons',
standaloneModule: 'react-iconpack-icons',
hasExports: true,
deps: {}
};
if(packer.opts.mode === 'png') {
packer.compilePNGs.call(packer, function PNGComplete(code) {
icons.source = code;
stream.push(icons);
next();
});
} else {
icons.source = packer.compileSVGs.call(packer);
stream.push(icons);
next();
}
});
});
}
return through.obj(write, end);
};
/**
* Compiles the currently loaded SVGs into a JS module, in preparation for
* injection into the bundle. The outputted code is optimized slightly readability,
* for debugging and so on. SVG and PNG module output are slightly different structure-wise
* as with SVG we attach some extra data (viewBox) that PNGs don't need.
*
* Side note: yes, parsing "HTML/XML/etc" with regex is dumb. This is also totally fine
* for right now though.
*
* @returns {String} The source for this "module" as a String, which Browserify needs.
*/
IconPacker.prototype.compileSVGs = function() {
var keys = Object.keys(this.SVGS),
key,
i = 0,
l = keys.length,
viewBoxRegex = /<svg.*?viewBox="(.*?)"/,
matches;
var code = 'module.exports = {\n mode: "svg",\n\n';
code += ' icons: {\n';
for(; i < l; i++) {
key = keys[i];
var viewBox = null;
matches = viewBoxRegex.exec(this.SVGS[key].data);
if(matches)
viewBox = matches[1];
code += ' "' + key + '": {\n';
code += ' props: {viewBox: ' + (viewBox ? '"' + viewBox + '"' : null) + '},\n';
code += " data: '" + this.SVGS[key].data.replace('<\/svg>', '')
.replace(/<\s*svg.*?>/ig, '') + "',\n},\n";
} else if(engine === 'browserify') {
return {
attachBrowserifyInjector: function(browserify, opts) {
return attachBrowserifyInjector.call(packer, browserify, opts);
}
};
} else if(engine.types) { // babel.types
return BabelSVGTracker.call(packer, engine);
}
return code + '}\n};\n';
// This should only ever be called for the icons module itself, and for Webpack only.
// With Browserify we're able to just inject things properly.
WebpackPlugin.loader.call(this, engine, packer);
};
/**
* Converts the loaded SVGS into PNGs then returns a JS module, in preparation for
* injection into the bundle.
*
* Somewhat confusingly, though, both compile___() methods in this library "return"
* Strings, because Browserify needs that.
*
* @param {Function} hollaback Called when all the PNGs be converted and the code's done.
*/
IconPacker.prototype.compilePNGs = function(hollaback) {
var packer = this,
keys = Object.keys(this.SVGS);
async.map(keys, packer.convertSVGtoPNG.bind(packer), function(error, pngs) {
var i = 0,
l = keys.length,
code = 'module.exports = {\n mode: "png",\n icons: {\n';
for(; i < l; i++) {
code += ' "' + keys[i] + '": "' + pngs[i] + '",\n';
}
// Remove that dangling "," from the end I guess. Probably don't need to
// as Babel/et al would handle it but whatever.
hollaback(code.slice(0, -1) + '\n }\n};\n');
});
};
/**
* A method that uses ImageMagick to convert SVGs to PNGs. This scratches a personal
* itch of the author, who really doesn't feel like running a headless browser in the
* background for a task as simple as converting a bunch of images.
*
* If you experience bad quality conversions with ImageMagick, you likely need to
* install it with librsvg support. For instance, on a Mac with homebrew:
*
* brew install imagemagick --with-librsvg
*
* Ta-da, you should be getting solid conversions now. If you routinely have issues
* with this you should be able to easily monkeypatch this to use something like
* Phantom or... something. See the docs for details.
*
* @param {String} key A key for the internal SVGS object.
* @param {Function} callback A callback that runs when the PNG is ready.
*/
IconPacker.prototype.convertSVGtoPNG = function(key, callback) {
var packer = this,
xml = '<?xml version="1.0" encoding="utf-8"?>',
patch = '<svg width="32" height="32" preserveAspectRatio="xMidYMid meet" ',
svg = new Buffer(xml + this.SVGS[key].data.replace('<svg ', patch));
console.log(svg.toString());
// I'll be honest, chaining APIs annoy me - just lemme use an options object.
im(svg).background(this.opts.png.background).quality(this.opts.png.quality)
.antialias(this.opts.png.antialias).density(this.opts.png.density)
.resize(this.opts.png.width).trim()
.toBuffer('PNG', function(error, buffer) {
if(error) {
console.warn('Warning: Could not convert ' + key + ' - ' + error);
return callback(error, buffer);
}
// Optimize and convert the buffer to base64 data
var quanted = pngquant ? pngquant.compress(buffer, {
speed: packer.opts.png.speed,
quality: packer.opts.png.compressorQuality,
iebug: packer.opts.png.ie6fix
}) : buffer;
callback(error, quanted.toString('base64'));
});
};
// You get that thing I sent ya?
module.exports = IconPacker;

30
lib/BabelSVGTracker.js Normal file
View file

@ -0,0 +1,30 @@
/**
* A Babel "plugin/transformer" that just tracks unique <Icon.../>
* JSX tags in your source code. Note that this does absolutely no
* modifications - just accumulates the unique SVG uris for the mapping.
*
* @param {Object} babel This will be auto-passed, most likely, by Babel.
* @returns {Object} babel.Transformer used for the babel'ing. You know the one.
*/
module.exports = function(babel) {
var _packer = this;
return {
visitor: {
JSXElement: function JSXElement(node, parent, scope, file) {
if(_packer.opts.JSXTagNames.indexOf(node.node.openingElement.name.name) < 0)
return;
var attributes = node.node.openingElement.attributes,
l = attributes.length,
i = 0;
for(; i < l; i++) {
if(attributes[i].name.name !== 'uri')
continue;
_packer.SVGS[attributes[i].value.value] = 1;
}
}
}
};
};

214
lib/IconPacker.js Normal file
View file

@ -0,0 +1,214 @@
/**
* iconpacker.js
*
* This just encapsulates the core icon-packer functionality. index.js
* is the main entry point to care about insofar as usage.
*
* @author Ryan McGrath <ryan@rymc.io
*/
var SVGO = require('svgo'),
fs = require('fs'),
async = require('async'),
through = require('through2'),
merge = require('lodash').merge,
cache = require('./cache/fs-cache.js'),
ReactSVGComponentFilePath = require.resolve('../components/svg.js');
/**
* IconPackerer constructor.
*
* @constructor
* @param {Object} options Options
* @return {Object} An instance of IconPacker to do things with.
*/
var IconPacker = function(options) {
if(!(this instanceof IconPacker))
return new IconPacker(options);
// This is a reference to the directory where we've got
// provided SVG files held. The user can specify a root directory of
// their own in the options below; we'll always check theirs first, falling
// back to checking ours if there's no hit.
this._internalSVGDirectory = __dirname + '/../svgs/';
// This is our grand database of SVGs. We do some caching here to see if we have
// an existing batch of SVGs loaded.
this.SVGS = {};
// Configure our options, if applied here
this.opts = {};
this.update(options);
// Basic binding() and such for pieces we use later on in the lifepsan
// of this object. See the respective methods for more details.
this.svgo = new SVGO(this.opts.svgo);
this.readAndOptimizeSVGs = this._readAndOptimizeSVGs.bind(this);
return this;
};
IconPacker.prototype.update = function(options) {
this.opts = merge({
verbose: false,
// JSX tags to pick up
JSXTagNames: ['Icon'],
// We provide some default SVGO options that work well for the icons in this
// project; users can override but keep in mind that it's ill-advised at the time
// of writing this to remove the viewBox attributes. This may change in a later release.
//
// (Removing the viewBox can cause very wonky issues in IE, and it helps to size the various icons).
svgSourceDirectory: '',
svgo: {
plugins: [
{removeViewBox: false}, // See note above ---^
{removeUselessStrokeAndFill: false}, // This can clobber some icons, just leave it.
{removeEmptyAttrs: false}, // I find this helpful, others may not.
{removeDimensions: true}
]
}
}, this.opts, options);
return this;
};
/**
* Handles async reading in all the SVG files and optimizing them. Really
* nothing too special here - just async.map'd later on down the file.
*
* @param {Function} callback Upon completion this junks runs.
*/
IconPacker.prototype._readAndOptimizeSVGs = function(key, callback) {
var packer = this,
internalFilePath = this._internalSVGDirectory + key + '.svg',
filePath = packer.opts.svgSourceDirectory + key + '.svg';
var onSVGOComplete = function(result) {
packer.SVGS[key] = result;
callback(null, result);
};
fs.readFile(filePath, 'utf8', function(error, data) {
if(!error)
return packer.svgo.optimize(data, onSVGOComplete);
fs.readFile(internalFilePath, 'utf8', function(e, d) {
if(!e)
return packer.svgo.optimize(d, onSVGOComplete);
console.warn('Warning: Could not load ' + key);
delete packer.SVGS[key]; // Better to error out on the client, I think
return callback(e, null);
});
});
};
/**
* Handles actually compiling the SVG assets into a module Browserify module, then passing
* it into the build stream for inclusion in the final bundle. I'll note that this
* was a massive PITA to decipher how to do - comments are your friends, Node devs.
*
* This is the only method that is "callback-hell"-ish, but I've chosen not to break
* it up because it still fits on one screen of code and is follow-able enough.
*
* @returns {Function} A through2 stream in object mode, which Browserify needs.
*/
IconPacker.prototype.compileForBrowserify = function() {
var packer = this,
write = function(buf, enc, next) {
next(null, buf);
};
var end = function(next) {
var keys = Object.keys(packer.SVGS),
stream = this;
async.map(keys, packer.readAndOptimizeSVGs, function(error, results) {
fs.readFile(ReactSVGComponentFilePath, 'utf8', function(e, data) {
stream.push({
id: 'react-iconpack',
externalRequireName: 'react-iconpack',
standaloneModule: 'react-iconpack',
hasExports: true,
source: data,
// An empty deps object is important here as we don't want to
// accidentally bundle a second copy of React or the icons as
// they're require()'d in the source. Don't change this.
deps: {}
});
var icons = {
id: 'react-iconpack-icons',
externalRequireName: 'react-iconpack-icons',
standaloneModule: 'react-iconpack-icons',
hasExports: true,
deps: {}
};
icons.source = packer.compileSVGs.call(packer);
stream.push(icons);
next();
});
});
}
return through.obj(write, end);
};
/**
* Whereas Browserify requires a really annoying Stream setup, Webpack is much more
* straightforward. Thus, internally there's two separate compile methods for this stuff.
*
* @param {Function} callbackfn A callback for when the module is ready, as it's async in nature.
* @returns {String} module The bundled module.
*/
IconPacker.prototype.compileForWebpack = function(callbackfn) {
var keys = Object.keys(this.SVGS);
async.map(keys, this.readAndOptimizeSVGs, function(error, results) {
callbackfn(this.compileSVGs());
}.bind(this));
};
/**
* Compiles the currently loaded SVGs into a JS module, in preparation for
* injection into the bundle. The outputted code is optimized slightly for readability,
* debugging and so on.
*
* Side note: yes, parsing "HTML/XML/etc" with regex is dumb. This is also totally fine
* for right now though.
*
* @returns {String} The source for this "module" as a String, which Browserify needs.
*/
IconPacker.prototype.compileSVGs = function() {
var keys = Object.keys(this.SVGS),
key,
i = 0,
l = keys.length,
viewBoxRegex = /<svg.*?viewBox="(.*?)"/,
matches;
var code = 'module.exports = {\n';
code += ' icons: {\n';
for(; i < l; i++) {
key = keys[i];
var viewBox = null;
matches = viewBoxRegex.exec(this.SVGS[key].data);
if(matches)
viewBox = matches[1];
code += ' "' + key + '": {\n';
code += ' props: {viewBox: ' + (viewBox ? '"' + viewBox + '"' : null) + '},\n';
code += " data: '" + (this.SVGS[key].data ? this.SVGS[key].data.replace('<\/svg>', '')
.replace(/<\s*svg.*?>/ig, '') : "") + "',\n},\n";
}
return code + '}\n};\n';
};
module.exports = IconPacker;

103
lib/WebpackPlugin.js Normal file
View file

@ -0,0 +1,103 @@
/**
* WebpackPlugin.js
*
* The Webpack plugin for react-iconpack. Manages intercepting the
* build/loader/etc processes and injecting the SVG icon set as a module.
*
* @author Ryan McGrath <ryan@rymc.io>
*/
var path = require('path'),
ReactSVGComponentFilePath = require.resolve('../components/svg.js'),
IconStubComponentPath = require.resolve('../components/react-iconpack-icons.js');
/**
* WebpackPlugin
*
* Accepts a packer, and options for configuring the packer instance itself.
* @Note: You likely don't want this, but the API in index.js.
*
* @param {Object} packer An instance of IconPacker (see lib/IconPacker.js).
* @param {Object} opts Options for configuring IconPacker (for certain use cases it's ideal to update here).
* @returns {Object} WebpackPlugin A new Webpack plugin.
*/
var WebpackPlugin = function(packer, opts) {
this.packer = packer;
this.packer.update(opts);
return this;
};
/**
* WebpackPlugin.loader()
*
* This is Webpack-specific functionality, which is why it's here - not looking
* to overly pollute index.js. This should be .call()'d with the loader context from
* index.js, along with the options.
*
* @param {Object} engine The engine object from index.js, which is just the source code.
* @param {Object} packer The packer object from index.js, shared amongst everything.
* @returns {void}
*/
WebpackPlugin.loader = function(engine, packer) {
var callback = this.async();
packer.compileForWebpack(function(source) {
callback(null, engine.replace('module.exports = {react_iconpack_icons: {}};', source));
});
};
/**
* This prototype.apply chain satisfies the Webpack plugin API. To be quite honest, the
* documentation surrounding Webpack plugin development is (almost, but not quite) as bad
* as Browserify. Really annoying to decipher.
*
* What we essentially do here is the following:
*
* - With each attempt to resolve a module, we check to see if it's one of our
* react-icon-* modules. With both of them, we redirect the request to a file in
* the component directory. This stops Webpack from blowing up and lets us provide
* a nicer API.
*
* - After module resolution is done, we check to see if the one being passed is
* the icons file. We specifically want to apply a loader (our index.js file) to
* replace the module source code with our true icons source code.
*
* This takes advantage of how Webpack consumes loaders. I originally wanted to make use
* of the compilation.addModule call but I could not figure out that undocumented... thing for
* the life of me (creating a RawModule and adding it did nothing).
*/
WebpackPlugin.prototype.apply = function(compiler) {
var packer = this.packer;
compiler.plugin('normal-module-factory', function(nmf) {
nmf.plugin('before-resolve', function(result, callback) {
if(!result)
return callback();
// We'll patch them in later... (see index.js)
if(/react-iconpack-icons$/.test(result.request))
result.request = IconStubComponentPath;
// Inject react-iconpack Component
// To do this we actually want to redirect the require statement directive
// to the proper file~
if(/react-iconpack$/.test(result.request))
result.request = ReactSVGComponentFilePath;
callback(null, result);
});
// This callback function parameter isn't even in the damn docs, just guessed at it...
// At any rate, if the icons module is detected we push our loader to the front of it
// What's nice is that we can do it ourselves and not require the user to do more configuration
// Note: this is called for every module, so the check is important.
nmf.plugin('after-resolve', function(data, callback) {
if(/react-iconpack-icons\.js/.test(data.request))
data.loaders.unshift(path.join(__dirname, '../index.js'));
callback(null, data);
});
});
};
module.exports = WebpackPlugin;

View file

@ -0,0 +1,27 @@
/**
* A Browserify plugin that hooks in to the Browserify build pipeline and injects
* your icons and a tag as a require()-able module.
*
* Originally I wanted to find a way to just have Babel shove this in, but I
* couldn't figure out a way to do it cleanly so this works.
*
* Note: you almost never need to call this yourself; you really just wanna pass
* it to Browserify instead. See the full documentation for more details.
*
* @param {Object} browserify An instance of Browserify for this to hook into.
* @param {Object} opts Options for the icon packer instance itself.
* @returns {Object} browserify The instance being operated on.
*/
module.exports = function(browserify, opts) {
this.update(opts);
var startListeningToThisCompleteMessOfALibraryAgain = function() {
browserify.pipeline.get('pack').unshift(this.compileForBrowserify());
}.bind(this);
browserify.external('react-iconpack');
browserify.external('react-iconpack-icons');
browserify.on('reset', startListeningToThisCompleteMessOfALibraryAgain);
startListeningToThisCompleteMessOfALibraryAgain();
return browserify;
};

136
lib/cache/fs-cache.js vendored Normal file
View file

@ -0,0 +1,136 @@
'use strict';
/**
* Filesystem cache
*
* Given a file and a transform function, cache the result into files
* or retrieve the previously cached files if the given file is already known.
*
* Note: This is shamelessly lifted and modified from babel-loader's cache. See
* https://github.com/babel/babel-loader/blob/master/lib/fs-cache.js
* -- Ryan McGrath, who'd rather not reinvent the wheel
*
* @see https://github.com/babel/babel-loader/issues/34
* @see https://github.com/babel/babel-loader/pull/41
*/
var crypto = require('crypto');
var mkdirp = require('mkdirp');
var fs = require('fs');
var os = require('os');
var path = require('path');
var zlib = require('zlib');
/**
* Read the contents from the compressed file.
*
* @async
* @params {String} filename
* @params {Function} callback
*/
var read = function(filename, callback) {
return fs.readFile(filename, function(err, data) {
if(err)
return callback(err);
return zlib.gunzip(data, function(err, content) {
var result = {};
if(err)
return callback(err);
try {
result = JSON.parse(content);
} catch (e) {
return callback(e);
}
return callback(null, result);
});
});
};
/**
* Write contents into a compressed file.
*
* @async
* @params {String} filename
* @params {String} result
* @params {Function} callback
*/
var write = function(filename, result, callback) {
var content = JSON.stringify(result);
return zlib.gzip(content, function(err, data) {
if(err)
return callback(err);
return fs.writeFile(filename, data, callback);
});
};
/**
* Build the filename for the cached file
*
* @params {String} source File source code
* @params {Object} options Options used
*
* @return {String}
*/
var filename = function(source, identifier, options) {
var hash = crypto.createHash('SHA1');
var contents = JSON.stringify({
source: source,
identifier: identifier,
});
hash.end(contents);
return hash.read().toString('hex') + '.json.gzip';
};
/**
* Retrieve file from cache, or create a new one for future reads
*
* @async
* @param {Object} params
* @param {String} params.identifier Unique identifier to bust cache
* @param {String} params.source Original contents of the file to be cached
* @param {Function<err, result>} callback
*
* @example
*
* cache({
* identifier: '',
* source: *source code from file*,
* }, function(err, result) {
*
* });
*/
var cache = module.exports = function(params, callback) {
// Spread params into named variables
// Forgive user whenever possible
var source = params.source;
var identifier = params.identifier;
var directory = os.tmpdir();
var file = path.join(directory, filename(source, identifier));
// Make sure the directory exists.
return mkdirp(directory, function(err) {
if(err)
return callback(err);
return read(file, function(err, content) {
var result = {};
// No errors mean that the file was previously cached
// we just need to return it
if(!err)
return callback(null, content);
// return it to the user asap and write it in cache
return write(file, result, function(err) {
return callback(err, result);
});
});
});
};

View file

@ -1,44 +1,56 @@
{
"name": "react-iconpack",
"description": "A React Component for handling SVGs coupled with Babel and Browserify plugins to only bundle the SVGs you use.",
"author": "Ryan McGrath",
"version": "1.1.4",
"license": "MIT",
"dependencies": {
"svgo": "^0.5.3",
"lodash": "*",
"through2": "^2.0.0",
"async": "^1.4.0"
},
"devDependencies": {
"jest": "",
"jasmine-node": ""
},
"scripts": {"test": "jest"},
"keywords": [
"react",
"react-component",
"browserify-plugin",
"babel-plugin",
"browserify",
"babel",
"svg",
"icons",
"polymer"
],
"maintainers": [{
"name": "Ryan McGrath",
"email": "ryan@venodesigns.net",
"web": "http://venodesigns.net/"
}],
"repository": {
"type": "git",
"url": "https://github.com/ryanmcgrath/react-iconpack.git"
},
"bugs": {"url": "https://github.com/ryanmcgrath/react-iconpack/issues"}
"name": "react-iconpack",
"description": "A React Component for handling SVGs coupled with Babel and Browserify plugins to only bundle the SVGs you use.",
"author": "Ryan McGrath",
"main": "index.js",
"version": "1.1.4",
"license": "MIT",
"dependencies": {
"async": "^1.4.0",
"lodash": "*",
"svgo": "^0.5.3",
"through2": "^2.0.0"
},
"devDependencies": {
"babel-core": "^6.7.2",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"babelify": "^7.2.0",
"browserify": "^13.0.0",
"gulp": "^3.9.1",
"jasmine-node": "",
"jest": "",
"react": "^0.14.7",
"react-dom": "^0.14.7",
"webpack": "^1.12.14"
},
"scripts": {
"test": "jest"
},
"keywords": [
"react",
"react-component",
"browserify-plugin",
"babel-plugin",
"browserify",
"babel",
"svg",
"icons",
"polymer"
],
"maintainers": [
{
"name": "Ryan McGrath",
"email": "ryan@venodesigns.net",
"web": "http://venodesigns.net/"
}
],
"repository": {
"type": "git",
"url": "https://github.com/ryanmcgrath/react-iconpack.git"
},
"bugs": {
"url": "https://github.com/ryanmcgrath/react-iconpack/issues"
}
}

19928
webpack-test/dist/build.js vendored Normal file

File diff suppressed because it is too large Load diff

12
webpack-test/index.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>react-iconpack Webpack Test</title>
</head>
<body>
<div id="test-node"></div>
<script src="dist/build.js" type="text/javascript"></script>
</body>
</html>

12
webpack-test/src/app.js Normal file
View file

@ -0,0 +1,12 @@
import React from 'react';
import {render} from 'react-dom';
import Icon from 'react-iconpack';
//!!
class TestComponent extends React.Component {
render() {
return <Icon uri="polymer/notification/disc_full" width="48" height="48" />;
}
};
render(<TestComponent />, document.getElementById('test-node'));

37
webpack.config.js Normal file
View file

@ -0,0 +1,37 @@
var IconPacker = require('react-iconpack')('webpack'),
path = require('path'),
PATHS = {
src: path.join(__dirname + '/webpack-test/src'),
dist: path.join(__dirname + '/webpack-test/dist'),
};
module.exports = {
cache: true,
entry: [path.join(PATHS.src, '/app.js')],
resolve: {
extensions: ['', '.js', '.jsx']
},
output: {
path: PATHS.dist,
filename: 'build.js'
},
module: {
loaders: [{
test: /\.jsx?$/,
loader: 'babel',
exclude: /node_modules/,
include: PATHS.src,
query: {
//cacheDirectory: true,
presets: ['es2015', 'react'],
plugins: ['react-iconpack']
}
}]
},
plugins: [IconPacker({})]
};