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:
parent
f2040233d0
commit
3801044178
18 changed files with 40032 additions and 486 deletions
30
lib/BabelSVGTracker.js
Normal file
30
lib/BabelSVGTracker.js
Normal 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
214
lib/IconPacker.js
Normal 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
103
lib/WebpackPlugin.js
Normal 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;
|
||||
27
lib/attachBrowserifyInjector.js
Normal file
27
lib/attachBrowserifyInjector.js
Normal 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
136
lib/cache/fs-cache.js
vendored
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
Reference in a new issue