| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 | /* eslint-disable import/no-extraneous-dependencies */const merge = require('deepmerge');const Promise = require('bluebird');const SVGCompiler = require('svg-baker');const spriteFactory = require('svg-baker/lib/sprite-factory');const Sprite = require('svg-baker/lib/sprite');const { NAMESPACE } = require('./config');const {  MappedList,  replaceInModuleSource,  replaceSpritePlaceholder,  getMatchedRule} = require('./utils');const defaultConfig = {  plainSprite: false,  spriteAttrs: {}};class SVGSpritePlugin {  constructor(cfg = {}) {    const config = merge.all([defaultConfig, cfg]);    this.config = config;    const spriteFactoryOptions = {      attrs: config.spriteAttrs    };    if (config.plainSprite) {      spriteFactoryOptions.styles = false;      spriteFactoryOptions.usages = false;    }    this.factory = ({ symbols }) => {      const opts = merge.all([spriteFactoryOptions, { symbols }]);      return spriteFactory(opts);    };    this.svgCompiler = new SVGCompiler();    this.rules = {};  }  /**   * This need to find plugin from loader context   */  // eslint-disable-next-line class-methods-use-this  get NAMESPACE() {    return NAMESPACE;  }  getReplacements() {    const isPlainSprite = this.config.plainSprite === true;    const replacements = this.map.groupItemsBySymbolFile((acc, item) => {      acc[item.resource] = isPlainSprite ? item.url : item.useUrl;    });    return replacements;  }  // TODO optimize MappedList instantiation in each hook  apply(compiler) {    this.rules = getMatchedRule(compiler);    const path = this.rules.outputPath ? this.rules.outputPath : this.rules.publicPath;    this.filenamePrefix = path      ? path.replace(/^\//, '')      : '';    if (compiler.hooks) {      compiler.hooks        .thisCompilation        .tap(NAMESPACE, (compilation) => {          compilation.hooks            .normalModuleLoader            .tap(NAMESPACE, loaderContext => loaderContext[NAMESPACE] = this);          compilation.hooks            .afterOptimizeChunks            .tap(NAMESPACE, () => this.afterOptimizeChunks(compilation));          compilation.hooks            .optimizeExtractedChunks            .tap(NAMESPACE, chunks => this.optimizeExtractedChunks(chunks));          compilation.hooks            .additionalAssets            .tapPromise(NAMESPACE, () => {              return this.additionalAssets(compilation);            });        });      compiler.hooks        .compilation        .tap(NAMESPACE, (compilation) => {          if (compilation.hooks.htmlWebpackPluginBeforeHtmlGeneration) {            compilation.hooks              .htmlWebpackPluginBeforeHtmlGeneration              .tapAsync(NAMESPACE, (htmlPluginData, callback) => {                htmlPluginData.assets.sprites = this.beforeHtmlGeneration(compilation);                callback(null, htmlPluginData);              });          }          if (compilation.hooks.htmlWebpackPluginBeforeHtmlProcessing) {            compilation.hooks              .htmlWebpackPluginBeforeHtmlProcessing              .tapAsync(NAMESPACE, (htmlPluginData, callback) => {                htmlPluginData.html = this.beforeHtmlProcessing(htmlPluginData);                callback(null, htmlPluginData);              });          }        });    } else {      // Handle only main compilation      compiler.plugin('this-compilation', (compilation) => {        // Share svgCompiler with loader        compilation.plugin('normal-module-loader', (loaderContext) => {          loaderContext[NAMESPACE] = this;        });        // Replace placeholders with real URL to symbol (in modules processed by svg-sprite-loader)        compilation.plugin('after-optimize-chunks', () => this.afterOptimizeChunks(compilation));        // Hook into extract-text-webpack-plugin to replace placeholders with real URL to symbol        compilation.plugin('optimize-extracted-chunks', chunks => this.optimizeExtractedChunks(chunks));        // Hook into html-webpack-plugin to add `sprites` variable into template context        compilation.plugin('html-webpack-plugin-before-html-generation', (htmlPluginData, done) => {          htmlPluginData.assets.sprites = this.beforeHtmlGeneration(compilation);          done(null, htmlPluginData);        });        // Hook into html-webpack-plugin to replace placeholders with real URL to symbol        compilation.plugin('html-webpack-plugin-before-html-processing', (htmlPluginData, done) => {          htmlPluginData.html = this.beforeHtmlProcessing(htmlPluginData);          done(null, htmlPluginData);        });        // Create sprite chunk        compilation.plugin('additional-assets', (done) => {          return this.additionalAssets(compilation)            .then(() => {              done();              return true;            })            .catch(e => done(e));        });      });    }  }  additionalAssets(compilation) {    const itemsBySprite = this.map.groupItemsBySpriteFilename();    const filenames = Object.keys(itemsBySprite);    return Promise.map(filenames, (filename) => {      const spriteSymbols = itemsBySprite[filename].map(item => item.symbol);      return Sprite.create({        symbols: spriteSymbols,        factory: this.factory      })        .then((sprite) => {          const content = sprite.render();          compilation.assets[`${this.filenamePrefix}${filename}`] = {            source() { return content; },            size() { return content.length; }          };        });    });  }  afterOptimizeChunks(compilation) {    const { symbols } = this.svgCompiler;    this.map = new MappedList(symbols, compilation);    const replacements = this.getReplacements();    this.map.items.forEach(item => replaceInModuleSource(item.module, replacements));  }  optimizeExtractedChunks(chunks) {    const replacements = this.getReplacements();    chunks.forEach((chunk) => {      let modules;      if (chunk.modulesIterable) {        modules = Array.from(chunk.modulesIterable);      } else {        modules = chunk.modules;      }      modules        // dirty hack to identify modules extracted by extract-text-webpack-plugin        // TODO refactor        .filter(module => '_originalModule' in module)        .forEach(module => replaceInModuleSource(module, replacements));    });  }  beforeHtmlGeneration(compilation) {    const itemsBySprite = this.map.groupItemsBySpriteFilename();    const sprites = Object.keys(itemsBySprite).reduce((acc, filename) => {      acc[this.filenamePrefix + filename] = compilation.assets[this.filenamePrefix + filename].source();      return acc;    }, {});    return sprites;  }  beforeHtmlProcessing(htmlPluginData) {    const replacements = this.getReplacements();    return replaceSpritePlaceholder(htmlPluginData.html, replacements);  }}module.exports = SVGSpritePlugin;
 |