travel/admin/node_modules/svg-sprite-loader/lib/plugin.js

234 lines
7.2 KiB
JavaScript

/* eslint-disable import/no-extraneous-dependencies */
const merge = require('deepmerge');
const Promise = require('bluebird');
const Chunk = require('webpack/lib/Chunk');
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,
getWebpackVersion,
getMatchedRule
} = require('./utils');
const webpackVersion = parseInt(getWebpackVersion(), 10);
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);
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();
const chunkName = filename.replace(/\.svg$/, '');
const chunk = new Chunk(chunkName);
chunk.ids = [];
chunk.files.push(filename);
const filenamePrefix = this.rules.publicPath
? this.rules.publicPath.replace(/^\//, '')
: '';
compilation.assets[`${filenamePrefix}${filename}`] = {
source() { return content; },
size() { return content.length; }
};
compilation.chunks.push(chunk);
});
});
}
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;
switch (webpackVersion) {
case 4:
modules = Array.from(chunk.modulesIterable);
break;
case 3:
modules = chunk.mapModules();
break;
default:
({ modules } = chunk);
break;
}
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[filename] = compilation.assets[filename].source();
return acc;
}, {});
return sprites;
}
beforeHtmlProcessing(htmlPluginData) {
const replacements = this.getReplacements();
return replaceSpritePlaceholder(htmlPluginData.html, replacements);
}
}
module.exports = SVGSpritePlugin;