plugin.js 6.9 KB

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