Skip to content

Commit f9eba1b

Browse files
committed
feat(spritehash): add ability to use [spritehash] substitution token in spriteFilename
1 parent 4c91db8 commit f9eba1b

3 files changed

Lines changed: 183 additions & 22 deletions

File tree

lib/plugin.js

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const Chunk = require('webpack/lib/Chunk');
44
const SVGCompiler = require('svg-baker');
55
const Sprite = require('svg-baker/lib/sprite');
66
const { NAMESPACE } = require('./config');
7-
const utils = require('./utils');
7+
const { MappedList, replaceInModuleSource } = require('./utils');
88

99
class SVGSpritePlugin {
1010
constructor() {
@@ -32,41 +32,35 @@ class SVGSpritePlugin {
3232

3333
// Replace placeholders with real URL to symbol (in modules processed by sprite-loader)
3434
compilation.plugin('after-optimize-chunks', function replacePlaceholdersInModules() {
35-
const map = utils.aggregate(symbols, this);
36-
const replacements = map.reduce((acc, item) => {
37-
acc[item.resource] = item.url;
38-
return acc;
39-
}, {});
40-
41-
map.forEach(item => utils.replaceInModuleSource(item.module, replacements));
35+
const map = new MappedList(symbols, this);
36+
const replacements = map.groupSpritesBySymbol((acc, item) => acc[item.resource] = item.url);
37+
map.items.forEach(item => replaceInModuleSource(item.module, replacements));
4238
});
4339

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

5245
chunks.forEach((chunk) => {
5346
chunk.modules
5447
// dirty hack to identify modules extracted by extract-text-webpack-plugin
5548
// TODO refactor
5649
.filter(module => '_originalModule' in module)
57-
.forEach(module => utils.replaceInModuleSource(module, replacements));
50+
.forEach(module => replaceInModuleSource(module, replacements));
5851
});
5952
});
6053

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

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

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

8175
compilation.chunks.push(chunk);
8276
});
83-
})
84-
.then(() => {
85-
done();
86-
return true;
87-
})
88-
.catch(e => done(e));
77+
}).then(() => {
78+
done();
79+
return true;
80+
}).catch(e => done(e));
8981
});
9082
});
9183
}

lib/utils/hash-tokens.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
const { getHashDigest } = require('loader-utils');
2+
3+
/**
4+
* Partially stolen from loader-utils#interpolateName
5+
* @param {string} input
6+
* @param {Object<string, string>} tokensToHash
7+
* @return {string}
8+
* @see https://github.com/webpack/loader-utils#interpolatename
9+
* @example
10+
* hashTokens('[chunkname]-[spritehash:6]', { spritehash: '<svg>...</svg>' })
11+
* // => '[chunkname]-8e04fd'
12+
*/
13+
module.exports = function hashTokens(input, tokensToHash) {
14+
let result = input;
15+
16+
Object.keys(tokensToHash).forEach((key) => {
17+
const content = tokensToHash[key];
18+
// eslint-disable-next-line no-useless-escape
19+
const re = new RegExp(`\\[(?:(\\w+):)?${key}(?::([a-z]+\\d*))?(?::(\\d+))?]`, 'ig');
20+
21+
result = result.replace(re, (all, hashType, digestType, maxLength) => {
22+
return getHashDigest(content, hashType, digestType, parseInt(maxLength, 10));
23+
});
24+
});
25+
26+
return result;
27+
};

lib/utils/mapped-list.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
const loaderDefaults = require('../config').loader;
2+
const getAllModules = require('./get-all-modules');
3+
const isModuleShouldBeExtracted = require('./is-module-should-be-extracted');
4+
const getLoaderOptions = require('./get-loader-options');
5+
const getLoadersRules = require('./get-loaders-rules');
6+
const getMatchedRule = require('./get-matched-rule');
7+
const getModuleChunk = require('./get-module-chunk');
8+
const hashTokens = require('./hash-tokens');
9+
10+
const spriteLoaderPath = require.resolve('../loader');
11+
12+
class MappedListItem {
13+
/**
14+
* @param {SpriteSymbol} symbol
15+
* @param {NormalModule} module
16+
* @param {string} spriteFilename
17+
*/
18+
constructor(symbol, module, spriteFilename) {
19+
this.symbol = symbol;
20+
this.module = module;
21+
this.spriteFilename = spriteFilename;
22+
this.resource = symbol.request.file;
23+
}
24+
25+
get url() {
26+
const { spriteFilename, symbol } = this;
27+
return `${spriteFilename}#${symbol.id}`;
28+
}
29+
30+
get useUrl() {
31+
const { spriteFilename, symbol } = this;
32+
return `${spriteFilename}#${symbol.useId}`;
33+
}
34+
}
35+
36+
class MappedList {
37+
/**
38+
* @param {SpriteSymbol[]} symbols
39+
* @param {Compilation} compilation
40+
*/
41+
constructor(symbols, compilation) {
42+
const { compiler } = compilation;
43+
44+
this.symbols = symbols;
45+
this.rules = getLoadersRules(compiler);
46+
this.allModules = getAllModules(compilation);
47+
this.spriteModules = this.allModules.filter(isModuleShouldBeExtracted);
48+
49+
this.items = this.create();
50+
}
51+
52+
/**
53+
* @param {MappedListItem[]} data
54+
* @return {Object<string, MappedListItem>}
55+
*/
56+
static groupSymbolsBySprite(data) {
57+
return data
58+
.map(item => item.spriteFilename)
59+
.filter((value, index, self) => self.indexOf(value) === index)
60+
.reduce((acc, spriteFilename) => {
61+
acc[spriteFilename] = data.filter(item => item.spriteFilename === spriteFilename);
62+
return acc;
63+
}, {});
64+
}
65+
66+
/**
67+
* @param {MappedListItem[]} data
68+
* @param {Function} [mapper] Custom grouper function
69+
* @return {Object<string, MappedListItem>}
70+
*/
71+
static groupSpritesBySymbol(data, mapper) {
72+
return data.reduce((acc, item) => {
73+
if (mapper) {
74+
mapper(acc, item);
75+
} else {
76+
acc[item.resource] = item;
77+
}
78+
return acc;
79+
}, {});
80+
}
81+
82+
/**
83+
* @return {MappedListItem[]}
84+
*/
85+
create() {
86+
const { symbols, spriteModules, allModules, rules } = this;
87+
const data = symbols.reduce((acc, symbol) => {
88+
const resource = symbol.request.file;
89+
const module = spriteModules.find(m => m.resource === resource);
90+
const rule = getMatchedRule(resource, rules);
91+
const options = rule ? getLoaderOptions(spriteLoaderPath, rule) : null;
92+
let spriteFilename = (options && options.spriteFilename)
93+
? options.spriteFilename
94+
: loaderDefaults.spriteFilename;
95+
96+
const chunk = module ? getModuleChunk(module, allModules) : null;
97+
if (chunk) {
98+
spriteFilename = spriteFilename.replace('[chunkname]', chunk.name);
99+
}
100+
101+
if (rule && module) {
102+
acc.push(new MappedListItem(symbol, module, spriteFilename));
103+
}
104+
105+
return acc;
106+
}, []);
107+
108+
// Additional pass to interpolate hash in spriteFilename
109+
const itemsBySpriteFilename = MappedList.groupSymbolsBySprite(data);
110+
const filenames = Object.keys(itemsBySpriteFilename);
111+
112+
filenames.forEach((filename) => {
113+
const items = itemsBySpriteFilename[filename];
114+
const spriteSymbols = items.map(item => item.symbol);
115+
const content = spriteSymbols.map(s => s.render()).join('');
116+
const interpolatedName = hashTokens(filename, { spritehash: content });
117+
118+
items
119+
.filter(item => item.spriteFilename !== interpolatedName)
120+
.forEach(item => item.spriteFilename = interpolatedName);
121+
});
122+
123+
return data;
124+
}
125+
126+
/**
127+
* @return {Object<string, MappedListItem>}
128+
*/
129+
groupSymbolsBySprite() {
130+
return MappedList.groupSymbolsBySprite(this.items);
131+
}
132+
133+
/**
134+
* @param {Function} [mapper] Custom grouper function
135+
* @return {Object<string, MappedListItem>}
136+
*/
137+
groupSpritesBySymbol(mapper) {
138+
return MappedList.groupSpritesBySymbol(this.items, mapper);
139+
}
140+
}
141+
142+
module.exports = MappedList;

0 commit comments

Comments
 (0)