1 |
|
2 | const merge = require('deepmerge');
|
3 | const Promise = require('bluebird');
|
4 | const SVGCompiler = require('svg-baker');
|
5 | const spriteFactory = require('svg-baker/lib/sprite-factory');
|
6 | const Sprite = require('svg-baker/lib/sprite');
|
7 | const { NAMESPACE } = require('./config');
|
8 | const {
|
9 | MappedList,
|
10 | replaceInModuleSource,
|
11 | replaceSpritePlaceholder,
|
12 | getWebpackVersion,
|
13 | getMatchedRule
|
14 | } = require('./utils');
|
15 |
|
16 | const webpackVersion = parseInt(getWebpackVersion(), 10);
|
17 |
|
18 | const defaultConfig = {
|
19 | plainSprite: false,
|
20 | spriteAttrs: {}
|
21 | };
|
22 |
|
23 | class 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 |
|
48 |
|
49 |
|
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 |
|
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 |
|
114 | compiler.plugin('this-compilation', (compilation) => {
|
115 |
|
116 | compilation.plugin('normal-module-loader', (loaderContext) => {
|
117 | loaderContext[NAMESPACE] = this;
|
118 | });
|
119 |
|
120 |
|
121 | compilation.plugin('after-optimize-chunks', () => this.afterOptimizeChunks(compilation));
|
122 |
|
123 |
|
124 | compilation.plugin('optimize-extracted-chunks', chunks => this.optimizeExtractedChunks(chunks));
|
125 |
|
126 |
|
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 |
|
134 | compilation.plugin('html-webpack-plugin-before-html-processing', (htmlPluginData, done) => {
|
135 | htmlPluginData.html = this.beforeHtmlProcessing(htmlPluginData);
|
136 | done(null, htmlPluginData);
|
137 | });
|
138 |
|
139 |
|
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 |
|
204 |
|
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 |
|
229 | module.exports = SVGSpritePlugin;
|