plugin.js 7.0 KB

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