plugin.js 7.2 KB

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