svg-spritemap-webpack-plugin
Version:
Generates symbol-based SVG spritemap from all .svg files in a directory
268 lines (267 loc) • 14.2 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import fs from 'node:fs';
import path from 'node:path';
import webpack from 'webpack';
import loaderUtils from 'loader-utils';
import { glob } from 'glob';
import { intersection, uniq, escapeRegExp } from 'lodash-es';
// Helpers
import { getTemplate } from './helpers/template.js';
import { hasVariables } from './helpers/variables.js';
import { applyEntryPlugin } from './helpers/entry.js';
import { generateSVG, optimizeSVG } from './helpers/svg.js';
import { generateStyles, getStylesType } from './helpers/styles.js';
import { formatOptions, isOptionsWithStyles } from './helpers/options.js';
// Constants
import { PLUGIN } from './constants.js';
// Types
import { Output, StylesType } from './types.js';
class SVGSpritemapPlugin {
constructor(patterns = '**/*.svg', options = {}) {
this.sources = {};
this.warnings = [];
this.filenames = {
spritemap: undefined,
styles: undefined
};
this.output = {
spritemap: undefined,
styles: undefined
};
this.cache = {
spritemap: undefined,
styles: undefined
};
this.dependencies = {
files: [],
directories: []
};
this.make = (compilation) => {
this.updateFilenames();
if (this.output.spritemap) {
compilation.emitAsset(this.options.output.filename, new webpack.sources.RawSource(this.output.spritemap), {
immutable: this.cache.spritemap === this.output.spritemap,
development: false,
javascriptModule: false
});
this.cache.spritemap = this.output.spritemap;
this.updateFilename(Output.Spritemap, compilation);
this.generateStyles(compilation);
}
compilation.hooks.afterHash.tap(PLUGIN, () => {
this.updateFilename(Output.Spritemap, compilation);
if (isOptionsWithStyles(this.options) && this.output.styles !== undefined && getStylesType(this.output.styles, this.options.styles.filename) === StylesType.Asset) {
compilation.emitAsset(this.options.styles.filename, new webpack.sources.RawSource(this.output.styles));
}
});
compilation.hooks.processAssets.tap({
name: this.constructor.name,
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE
}, () => {
var _a;
if (!this.filenames.spritemap) {
return;
}
const source = (_a = compilation.getAsset(this.filenames.spritemap)) === null || _a === void 0 ? void 0 : _a.source.source().toString();
if (!source) {
return;
}
compilation.updateAsset(this.filenames.spritemap, new webpack.sources.RawSource(optimizeSVG(source, this.options)), {
minimized: !!this.options.output.svgo
});
compilation.chunks.forEach((chunk) => {
if (!this.filenames.spritemap || chunk.name !== this.options.output.chunk.name) {
return;
}
chunk.files.add(this.filenames.spritemap);
});
});
};
this.generateSpritemap = (compiler) => __awaiter(this, void 0, void 0, function* () {
const sprites = Object.values(this.dependencies).flat();
const modifiedFiles = compiler.modifiedFiles ? [...compiler.modifiedFiles] : [];
if (modifiedFiles.length && !intersection(sprites, modifiedFiles).length) {
return;
}
this.sources = Object.fromEntries(yield Promise.all(this.dependencies.files.map((location) => __awaiter(this, void 0, void 0, function* () {
return [location, yield fs.promises.readFile(location, 'utf8')];
}))));
if (!Object.keys(this.sources).length) {
this.warnings.push(new webpack.WebpackError(`No SVG files found in the specified patterns: ${this.patterns.join(', ')}`));
}
if (this.options.sprite.generate.view && !this.options.sprite.generate.use) {
this.warnings.push(new webpack.WebpackError(`Using sprite.generate.view requires sprite.generate.use to be enabled`));
}
if (this.options.sprite.generate.use && !this.options.sprite.generate.symbol) {
this.warnings.push(new webpack.WebpackError(`Using sprite.generate.use requires sprite.generate.symbol to be enabled`));
}
if (this.options.sprite.generate.title && !this.options.sprite.generate.symbol) {
this.warnings.push(new webpack.WebpackError(`Using sprite.generate.title requires sprite.generate.symbol to be enabled`));
}
if (this.options.sprite.generate.symbol === true && this.options.sprite.generate.view === true) {
this.warnings.push(new webpack.WebpackError('Both sprite.generate.symbol and sprite.generate.view are set to true which will cause identifier conflicts, use a string value (postfix) for either of these options'));
}
this.output.spritemap = generateSVG(this.sources, this.options, this.warnings);
});
this.generateStyles = (compilation) => {
if (!isOptionsWithStyles(this.options)) {
return;
}
const extension = path.extname(this.options.styles.filename).slice(1).toLowerCase();
if (!['scss', 'sass'].includes(extension) && hasVariables(this.output.spritemap)) {
this.warnings.push(new webpack.WebpackError(`Variables are not supported when using ${extension.toUpperCase()}`));
}
if (this.options.styles.format === 'fragment' && hasVariables(this.output.spritemap)) {
this.warnings.push(new webpack.WebpackError(`Variables will not work when using styles.format set to 'fragment'`));
}
// Emit a warning when using 'fragment' for styles.format without enabling sprite.generate.view
if (this.options.styles.format === 'fragment' && !this.options.sprite.generate.view) {
this.warnings.push(new webpack.WebpackError(`Using styles.format with value 'fragment' in combination with sprite.generate.view with value false will result in CSS fragments not working correctly`));
}
// Emit a warning when using [hash] in filename while using 'fragment' for styles.format
if (this.options.styles.format === 'fragment' && this.options.output.filename.includes('[hash]')) {
this.warnings.push(new webpack.WebpackError(`Using styles.format with value 'fragment' in combination with [hash] in output.filename will result in incorrect fragment URLs`));
}
this.output.styles = generateStyles(this.output.spritemap, this.options, this.warnings, compilation);
};
this.injectEntry = (compiler, context, entry) => {
if (!this.options.output.svg4everybody) {
return;
}
const template = getTemplate('svg4everybody.js');
const output = path.resolve(import.meta.dirname, 'svg4everybody-helper.js');
fs.writeFileSync(output, template.replace('/* PLACEHOLDER */', JSON.stringify(this.options.output.svg4everybody)), 'utf8');
if (typeof entry === 'object') {
Object.keys(entry).forEach((name) => {
const subentry = entry[name];
if ('import' in subentry && subentry.import) {
applyEntryPlugin(compiler, context, [...subentry.import, output], name);
}
else {
throw new TypeError(`Unsupported sub-entry type for svg4everybody helper: '${typeof subentry}'`);
}
});
}
else {
throw new TypeError(`Unsupported entry type for svg4everybody helper: '${typeof entry}'`);
}
};
this.updateDependencies = () => {
this.dependencies.files = [];
this.dependencies.directories = [];
this.patterns.forEach((pattern) => {
const root = path.resolve(pattern.replace(/\*.*/, ''));
if (!path.basename(root).includes('.')) {
this.dependencies.directories.push(root);
}
glob.sync(pattern, this.options.input.options).map((match) => {
const pathname = path.resolve(match);
const stats = fs.lstatSync(pathname);
if (stats.isFile()) {
this.dependencies.files.push(pathname);
this.dependencies.directories.push(path.dirname(pathname));
}
else if (stats.isDirectory()) {
this.dependencies.directories.push(pathname);
}
});
});
if (!this.options.input.allowDuplicates) {
this.dependencies.files = uniq(this.dependencies.files);
this.dependencies.directories = uniq(this.dependencies.directories);
}
this.dependencies.files.sort();
this.dependencies.directories.sort();
};
this.updateContextDependencies = (compilation) => {
this.dependencies.directories.forEach((directory) => {
compilation.contextDependencies.add(directory);
});
};
this.updateWarnings = (compilation) => {
compilation.warnings = [...compilation.warnings, ...this.warnings];
};
this.updateFilenames = () => {
this.filenames.spritemap = this.options.output.filename;
if (isOptionsWithStyles(this.options)) {
this.filenames.styles = this.options.styles.filename;
}
};
this.updateFilename = (name, compilation) => {
var _a;
const oldFilename = this.filenames[name];
if (!oldFilename) {
return oldFilename;
}
const asset = compilation.getAsset(oldFilename);
if (!asset) {
return oldFilename;
}
const contenthash = loaderUtils.getHashDigest(asset.source.buffer(), 'sha1', 'hex', (_a = compilation.options.output.hashDigestLength) !== null && _a !== void 0 ? _a : 16);
const newFilename = Object.entries({
'[hash]': compilation.hash,
'[contenthash]': contenthash
}).reduce((filename, [pattern, value]) => {
return filename.replaceAll(new RegExp(escapeRegExp(pattern), 'ig'), value !== null && value !== void 0 ? value : pattern);
}, oldFilename);
compilation.renameAsset(oldFilename, newFilename);
compilation.updateAsset(newFilename, asset.source, {
contenthash: contenthash
});
this.filenames[name] = newFilename;
};
this.cleanup = (compilation) => {
if (this.options.output.chunk.keep) {
return;
}
compilation.hooks.processAssets.tap({
name: this.constructor.name,
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE
}, () => {
const filenames = compilation.options.plugins.filter((plugin) => {
return plugin instanceof SVGSpritemapPlugin;
}).map((plugin) => {
return Object.values(plugin.filenames);
}).reduce((filenames, values) => {
return [...filenames, ...values];
}, []);
[...compilation.chunks].filter((chunk) => {
if (!chunk.name) {
return false;
}
return chunk.name.startsWith(this.options.output.chunk.name);
}).forEach((chunk) => {
[...chunk.files].filter((file) => {
return !filenames.includes(file);
}).forEach((file) => {
delete compilation.assets[file]; // eslint-disable-line @typescript-eslint/no-dynamic-delete
});
});
});
};
this.patterns = Array.isArray(patterns) ? patterns : [patterns];
this.options = formatOptions(options);
this.updateFilenames();
}
apply(compiler) {
compiler.hooks.entryOption.tap(PLUGIN, this.injectEntry.bind(this, compiler));
compiler.hooks.environment.tap(PLUGIN, this.updateDependencies);
compiler.hooks.run.tapPromise(PLUGIN, this.generateSpritemap);
compiler.hooks.watchRun.tap(PLUGIN, this.updateDependencies);
compiler.hooks.watchRun.tapPromise(PLUGIN, this.generateSpritemap);
compiler.hooks.make.tap(PLUGIN, this.make);
compiler.hooks.thisCompilation.tap(PLUGIN, this.cleanup);
compiler.hooks.afterCompile.tap(PLUGIN, this.updateWarnings);
compiler.hooks.afterCompile.tap(PLUGIN, this.updateContextDependencies);
}
;
}
export default SVGSpritemapPlugin;