plugin.js 6.8 KB

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