UNPKG

svg-spritemap-webpack-plugin

Version:

Generates symbol-based SVG spritemap from all .svg files in a directory

268 lines (267 loc) 14.2 kB
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;