UNPKG

7.25 kBJavaScriptView Raw
1/* eslint-disable import/no-extraneous-dependencies */
2const merge = require('deepmerge');
3const Promise = require('bluebird');
4const SVGCompiler = require('svg-baker');
5const spriteFactory = require('svg-baker/lib/sprite-factory');
6const Sprite = require('svg-baker/lib/sprite');
7const { NAMESPACE } = require('./config');
8const {
9 MappedList,
10 replaceInModuleSource,
11 replaceSpritePlaceholder,
12 getWebpackVersion,
13 getMatchedRule
14} = require('./utils');
15
16const webpackVersion = parseInt(getWebpackVersion(), 10);
17
18const defaultConfig = {
19 plainSprite: false,
20 spriteAttrs: {}
21};
22
23class SVGSpritePlugin {
24 constructor(cfg = {}) {
25 const config = merge.all([defaultConfig, cfg]);
26 this.config = config;
27
28 const spriteFactoryOptions = {
29 attrs: config.spriteAttrs
30 };
31
32 if (config.plainSprite) {
33 spriteFactoryOptions.styles = false;
34 spriteFactoryOptions.usages = false;
35 }
36
37 this.factory = ({ symbols }) => {
38 const opts = merge.all([spriteFactoryOptions, { symbols }]);
39 return spriteFactory(opts);
40 };
41
42 this.svgCompiler = new SVGCompiler();
43 this.rules = {};
44 }
45
46 /**
47 * This need to find plugin from loader context
48 */
49 // eslint-disable-next-line class-methods-use-this
50 get NAMESPACE() {
51 return NAMESPACE;
52 }
53
54 getReplacements() {
55 const isPlainSprite = this.config.plainSprite === true;
56 const replacements = this.map.groupItemsBySymbolFile((acc, item) => {
57 acc[item.resource] = isPlainSprite ? item.url : item.useUrl;
58 });
59 return replacements;
60 }
61
62 // TODO optimize MappedList instantiation in each hook
63 apply(compiler) {
64 this.rules = getMatchedRule(compiler);
65
66 if (compiler.hooks) {
67 compiler.hooks
68 .thisCompilation
69 .tap(NAMESPACE, (compilation) => {
70 compilation.hooks
71 .normalModuleLoader
72 .tap(NAMESPACE, loaderContext => loaderContext[NAMESPACE] = this);
73
74 compilation.hooks
75 .afterOptimizeChunks
76 .tap(NAMESPACE, () => this.afterOptimizeChunks(compilation));
77
78 compilation.hooks
79 .optimizeExtractedChunks
80 .tap(NAMESPACE, chunks => this.optimizeExtractedChunks(chunks));
81
82 compilation.hooks
83 .additionalAssets
84 .tapPromise(NAMESPACE, () => {
85 return this.additionalAssets(compilation);
86 });
87 });
88
89 compiler.hooks
90 .compilation
91 .tap(NAMESPACE, (compilation) => {
92 if (compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration) {
93 compilation.hooks
94 .htmlWebpackPluginBeforeHtmlGeneration
95 .tapAsync(NAMESPACE, (htmlPluginData, callback) => {
96 htmlPluginData.assets.sprites = this.beforeHtmlGeneration(compilation);
97
98 callback(null, htmlPluginData);
99 });
100 }
101
102 if (compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing) {
103 compilation.hooks
104 .htmlWebpackPluginBeforeHtmlProcessing
105 .tapAsync(NAMESPACE, (htmlPluginData, callback) => {
106 htmlPluginData.html = this.beforeHtmlProcessing(htmlPluginData);
107
108 callback(null, htmlPluginData);
109 });
110 }
111 });
112 } else {
113 // Handle only main compilation
114 compiler.plugin('this-compilation', (compilation) => {
115 // Share svgCompiler with loader
116 compilation.plugin('normal-module-loader', (loaderContext) => {
117 loaderContext[NAMESPACE] = this;
118 });
119
120 // Replace placeholders with real URL to symbol (in modules processed by svg-sprite-loader)
121 compilation.plugin('after-optimize-chunks', () => this.afterOptimizeChunks(compilation));
122
123 // Hook into extract-text-webpack-plugin to replace placeholders with real URL to symbol
124 compilation.plugin('optimize-extracted-chunks', chunks => this.optimizeExtractedChunks(chunks));
125
126 // Hook into html-webpack-plugin to add `sprites` variable into template context
127 compilation.plugin('html-webpack-plugin-before-html-generation', (htmlPluginData, done) => {
128 htmlPluginData.assets.sprites = this.beforeHtmlGeneration(compilation);
129
130 done(null, htmlPluginData);
131 });
132
133 // Hook into html-webpack-plugin to replace placeholders with real URL to symbol
134 compilation.plugin('html-webpack-plugin-before-html-processing', (htmlPluginData, done) => {
135 htmlPluginData.html = this.beforeHtmlProcessing(htmlPluginData);
136 done(null, htmlPluginData);
137 });
138
139 // Create sprite chunk
140 compilation.plugin('additional-assets', (done) => {
141 return this.additionalAssets(compilation)
142 .then(() => {
143 done();
144 return true;
145 })
146 .catch(e => done(e));
147 });
148 });
149 }
150 }
151
152 additionalAssets(compilation) {
153 const itemsBySprite = this.map.groupItemsBySpriteFilename();
154 const filenames = Object.keys(itemsBySprite);
155
156 return Promise.map(filenames, (filename) => {
157 const spriteSymbols = itemsBySprite[filename].map(item => item.symbol);
158
159 return Sprite.create({
160 symbols: spriteSymbols,
161 factory: this.factory
162 })
163 .then((sprite) => {
164 const content = sprite.render();
165 const filenamePrefix = this.rules.publicPath
166 ? this.rules.publicPath.replace(/^\//, '')
167 : '';
168
169 compilation.assets[`${filenamePrefix}${filename}`] = {
170 source() { return content; },
171 size() { return content.length; }
172 };
173 });
174 });
175 }
176
177 afterOptimizeChunks(compilation) {
178 const { symbols } = this.svgCompiler;
179 this.map = new MappedList(symbols, compilation);
180 const replacements = this.getReplacements();
181 this.map.items.forEach(item => replaceInModuleSource(item.module, replacements));
182 }
183
184 optimizeExtractedChunks(chunks) {
185 const replacements = this.getReplacements();
186
187 chunks.forEach((chunk) => {
188 let modules;
189
190 switch (webpackVersion) {
191 case 4:
192 modules = Array.from(chunk.modulesIterable);
193 break;
194 case 3:
195 modules = chunk.mapModules();
196 break;
197 default:
198 ({ modules } = chunk);
199 break;
200 }
201
202 modules
203 // dirty hack to identify modules extracted by extract-text-webpack-plugin
204 // TODO refactor
205 .filter(module => '_originalModule' in module)
206 .forEach(module => replaceInModuleSource(module, replacements));
207 });
208 }
209
210 beforeHtmlGeneration(compilation) {
211 const itemsBySprite = this.map.groupItemsBySpriteFilename();
212 const filenamePrefix = this.rules.publicPath
213 ? this.rules.publicPath.replace(/^\//, '')
214 : '';
215 const sprites = Object.keys(itemsBySprite).reduce((acc, filename) => {
216 acc[filenamePrefix + filename] = compilation.assets[filenamePrefix + filename].source();
217 return acc;
218 }, {});
219
220 return sprites;
221 }
222
223 beforeHtmlProcessing(htmlPluginData) {
224 const replacements = this.getReplacements();
225 return replaceSpritePlaceholder(htmlPluginData.html, replacements);
226 }
227}
228
229module.exports = SVGSpritePlugin;