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;
|