Skip to content

Commit

Permalink
feat(spritehash): add ability to use [spritehash] substitution toke…
Browse files Browse the repository at this point in the history
…n in spriteFilename
  • Loading branch information
kisenka committed May 7, 2017
1 parent 4c91db8 commit f9eba1b
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 22 deletions.
36 changes: 14 additions & 22 deletions lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const Chunk = require('webpack/lib/Chunk');
const SVGCompiler = require('svg-baker');
const Sprite = require('svg-baker/lib/sprite');
const { NAMESPACE } = require('./config');
const utils = require('./utils');
const { MappedList, replaceInModuleSource } = require('./utils');

class SVGSpritePlugin {
constructor() {
Expand Down Expand Up @@ -32,41 +32,35 @@ class SVGSpritePlugin {

// Replace placeholders with real URL to symbol (in modules processed by sprite-loader)
compilation.plugin('after-optimize-chunks', function replacePlaceholdersInModules() {
const map = utils.aggregate(symbols, this);
const replacements = map.reduce((acc, item) => {
acc[item.resource] = item.url;
return acc;
}, {});

map.forEach(item => utils.replaceInModuleSource(item.module, replacements));
const map = new MappedList(symbols, this);
const replacements = map.groupSpritesBySymbol((acc, item) => acc[item.resource] = item.url);
map.items.forEach(item => replaceInModuleSource(item.module, replacements));
});

// Replace placeholders with real URL to symbol (in modules extracted by extract-text-webpack-plugin)
compilation.plugin('optimize-extracted-chunks', function replacePlaceholdersInExtractedChunks(chunks) {
const map = utils.aggregate(symbols, this);
const replacements = map.reduce((acc, item) => {
acc[item.resource] = item.useUrl;
return acc;
}, {});
const map = new MappedList(symbols, this);
const replacements = map.groupSpritesBySymbol((acc, item) => acc[item.resource] = item.useUrl);

chunks.forEach((chunk) => {
chunk.modules
// dirty hack to identify modules extracted by extract-text-webpack-plugin
// TODO refactor
.filter(module => '_originalModule' in module)
.forEach(module => utils.replaceInModuleSource(module, replacements));
.forEach(module => replaceInModuleSource(module, replacements));
});
});

// Create sprite chunk
compilation.plugin('additional-assets', function emitSpriteChunks(done) {
const sprites = utils.groupSymbolsBySprites(utils.aggregate(symbols, this));
const map = new MappedList(symbols, this);
const sprites = map.groupSymbolsBySprite();
const filenames = Object.keys(sprites);

return Promise.map(filenames, (spriteFilename) => {
const spriteSymbols = sprites[spriteFilename];

return Sprite.create({ symbols: spriteSymbols, filename: spriteFilename })
return Sprite.create({ symbols: spriteSymbols })
.then((sprite) => {
const content = sprite.render();
const chunk = new Chunk(spriteFilename);
Expand All @@ -80,12 +74,10 @@ class SVGSpritePlugin {

compilation.chunks.push(chunk);
});
})
.then(() => {
done();
return true;
})
.catch(e => done(e));
}).then(() => {
done();
return true;
}).catch(e => done(e));
});
});
}
Expand Down
27 changes: 27 additions & 0 deletions lib/utils/hash-tokens.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const { getHashDigest } = require('loader-utils');

/**
* Partially stolen from loader-utils#interpolateName
* @param {string} input
* @param {Object<string, string>} tokensToHash
* @return {string}
* @see https://github.com/webpack/loader-utils#interpolatename
* @example
* hashTokens('[chunkname]-[spritehash:6]', { spritehash: '<svg>...</svg>' })
* // => '[chunkname]-8e04fd'
*/
module.exports = function hashTokens(input, tokensToHash) {
let result = input;

Object.keys(tokensToHash).forEach((key) => {
const content = tokensToHash[key];
// eslint-disable-next-line no-useless-escape
const re = new RegExp(`\\[(?:(\\w+):)?${key}(?::([a-z]+\\d*))?(?::(\\d+))?]`, 'ig');

result = result.replace(re, (all, hashType, digestType, maxLength) => {
return getHashDigest(content, hashType, digestType, parseInt(maxLength, 10));
});
});

return result;
};
142 changes: 142 additions & 0 deletions lib/utils/mapped-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
const loaderDefaults = require('../config').loader;
const getAllModules = require('./get-all-modules');
const isModuleShouldBeExtracted = require('./is-module-should-be-extracted');
const getLoaderOptions = require('./get-loader-options');
const getLoadersRules = require('./get-loaders-rules');
const getMatchedRule = require('./get-matched-rule');
const getModuleChunk = require('./get-module-chunk');
const hashTokens = require('./hash-tokens');

const spriteLoaderPath = require.resolve('../loader');

class MappedListItem {
/**
* @param {SpriteSymbol} symbol
* @param {NormalModule} module
* @param {string} spriteFilename
*/
constructor(symbol, module, spriteFilename) {
this.symbol = symbol;
this.module = module;
this.spriteFilename = spriteFilename;
this.resource = symbol.request.file;
}

get url() {
const { spriteFilename, symbol } = this;
return `${spriteFilename}#${symbol.id}`;
}

get useUrl() {
const { spriteFilename, symbol } = this;
return `${spriteFilename}#${symbol.useId}`;
}
}

class MappedList {
/**
* @param {SpriteSymbol[]} symbols
* @param {Compilation} compilation
*/
constructor(symbols, compilation) {
const { compiler } = compilation;

this.symbols = symbols;
this.rules = getLoadersRules(compiler);
this.allModules = getAllModules(compilation);
this.spriteModules = this.allModules.filter(isModuleShouldBeExtracted);

this.items = this.create();
}

/**
* @param {MappedListItem[]} data
* @return {Object<string, MappedListItem>}
*/
static groupSymbolsBySprite(data) {
return data
.map(item => item.spriteFilename)
.filter((value, index, self) => self.indexOf(value) === index)
.reduce((acc, spriteFilename) => {
acc[spriteFilename] = data.filter(item => item.spriteFilename === spriteFilename);
return acc;
}, {});
}

/**
* @param {MappedListItem[]} data
* @param {Function} [mapper] Custom grouper function
* @return {Object<string, MappedListItem>}
*/
static groupSpritesBySymbol(data, mapper) {
return data.reduce((acc, item) => {
if (mapper) {
mapper(acc, item);
} else {
acc[item.resource] = item;
}
return acc;
}, {});
}

/**
* @return {MappedListItem[]}
*/
create() {
const { symbols, spriteModules, allModules, rules } = this;
const data = symbols.reduce((acc, symbol) => {
const resource = symbol.request.file;
const module = spriteModules.find(m => m.resource === resource);
const rule = getMatchedRule(resource, rules);
const options = rule ? getLoaderOptions(spriteLoaderPath, rule) : null;
let spriteFilename = (options && options.spriteFilename)
? options.spriteFilename
: loaderDefaults.spriteFilename;

const chunk = module ? getModuleChunk(module, allModules) : null;
if (chunk) {
spriteFilename = spriteFilename.replace('[chunkname]', chunk.name);
}

if (rule && module) {
acc.push(new MappedListItem(symbol, module, spriteFilename));
}

return acc;
}, []);

// Additional pass to interpolate hash in spriteFilename
const itemsBySpriteFilename = MappedList.groupSymbolsBySprite(data);
const filenames = Object.keys(itemsBySpriteFilename);

filenames.forEach((filename) => {
const items = itemsBySpriteFilename[filename];
const spriteSymbols = items.map(item => item.symbol);
const content = spriteSymbols.map(s => s.render()).join('');
const interpolatedName = hashTokens(filename, { spritehash: content });

items
.filter(item => item.spriteFilename !== interpolatedName)
.forEach(item => item.spriteFilename = interpolatedName);
});

return data;
}

/**
* @return {Object<string, MappedListItem>}
*/
groupSymbolsBySprite() {
return MappedList.groupSymbolsBySprite(this.items);
}

/**
* @param {Function} [mapper] Custom grouper function
* @return {Object<string, MappedListItem>}
*/
groupSpritesBySymbol(mapper) {
return MappedList.groupSpritesBySymbol(this.items, mapper);
}
}

module.exports = MappedList;

0 comments on commit f9eba1b

Please sign in to comment.