'use strict'; var fs = require('node:fs'); var node_process = require('node:process'); var pluginutils = require('@rollup/pluginutils'); var deepmerge = require('deepmerge'); var svgo = require('svgo'); var path = require('node:path'); /** * Create svg sprite with symbols * * @param symbols {string[]} * @param inline {boolean} whether to inline the sprite * @returns {string} */ function createSvgSprite(symbols, id) { return `${symbols.join("")}`; } /** * Create svg symbol from svg content * * @param svg {string} svg content * @param id {string} symbol id * * @returns {string} symbol */ function createSvgSymbol(svg, id) { // replace svg tag with symbol tag return svg.replace(/]*)>/, ``).replace("", ""); } /** * Get file path relative to baseDir * * @param moduleId {string} the module id of the file * @param baseDir {string} the base directory * * @returns {string} the relative file path */ function getFilePath(moduleId, baseDir) { return baseDir ? path.relative(baseDir, moduleId).replaceAll(path.win32.sep, path.posix.sep) : moduleId; } /** * Get symbol id with template * * @param filePath {string} the file path * @param template {string} the symbolId template * * @returns {string} */ function getSymbolId(filePath, template) { let symbolId = template; if (symbolId.includes("[name]")) { symbolId = symbolId.replaceAll("[name]", path.basename(filePath, path.extname(filePath))); } if (symbolId.includes("[dirname]")) { const dir = path.dirname(filePath); symbolId = symbolId.replaceAll("[dirname]", dir.replace(":", "").split(path.posix.sep).filter(Boolean).join("-")); } return symbolId; } /** * Check if the module id is a svg file path. * * @param moduleId {string} id to check * @returns {boolean} */ function isSvgFilePath(moduleId) { if (!moduleId) return false; const queryIndex = moduleId.lastIndexOf("?"); if (queryIndex !== -1) { moduleId = moduleId.slice(0, queryIndex); } return moduleId.endsWith(".svg"); } const defaultSvgSpriteId = "svg-sprite"; const defaultSymbolId = "[name]"; const defaultFileName = "svg-sprite.svg"; const defaultSvgoConfig = { plugins: [{ name: "preset-default", params: { overrides: { removeViewBox: false // keep viewBox attr } } }, "cleanupIds", // clean up ids, we will use symbol id instead "removeDimensions", // remove width/height attributes, set viewBox instead "removeXMLNS" // remove xmlns attribute ] }; const defaultBaseDir = node_process.cwd(); /** * Normalize svgo config, load config from file if config is a string. * * @param config {string | SvgoConfig} svgo config * @returns {Promise} */ async function normalizeSvgoConfig(config) { if (!config) { return defaultSvgoConfig; } if (typeof config === "string") { const c = await svgo.loadConfig(config); return deepmerge(defaultSvgoConfig, c); } return deepmerge(defaultSvgoConfig, config); } /** * Normalize symbol id function. * * @param symbolId {string | SymbolIdFn} symbol id or symbol id function * @returns {SymbolIdFn} */ function normalizeSymbolIdFunction(symbolId) { return typeof symbolId === "function" ? symbolId : () => symbolId ?? defaultSymbolId; } /** * Normalize base dir function. * * @param baseDir {string | BaseDirFunction} base dir or base dir function * @returns {BaseDirFunction} */ function normalizeBaseDirFunction(baseDir) { if (baseDir === undefined) { return () => defaultBaseDir; } return typeof baseDir === "function" ? baseDir : () => baseDir; } async function svgCombiner(options = {}) { const filter = pluginutils.createFilter(options.include, options.exclude); const baseDirFunction = normalizeBaseDirFunction(options.baseDir); const symbolIdFunction = normalizeSymbolIdFunction(options.symbolId); const svgoConfig = await normalizeSvgoConfig(options.svgoConfig); const svgSymbols = new Map(); return { name: "vite:svg-combiner", enforce: "pre", async load(moduleId) { if (!isSvgFilePath(moduleId) || !filter(moduleId)) { // ignore, other load function will handle it return null; } // load svg file as text const code = await fs.promises.readFile(moduleId, "utf8"); return { code, map: { mappings: "" } }; }, transform(code, moduleId) { var _svgSymbols$get; if (!isSvgFilePath(moduleId) || !filter(moduleId)) { return null; } const baseDir = baseDirFunction(moduleId); const filePath = getFilePath(moduleId, baseDir); const symbolIdTemplate = symbolIdFunction(filePath); if (!symbolIdTemplate) { this.error(`Symbol id is empty, please check your symbolId option.`); } const symbolId = getSymbolId(filePath, symbolIdTemplate); if (svgSymbols.has(symbolId) && moduleId !== ((_svgSymbols$get = svgSymbols.get(symbolId)) === null || _svgSymbols$get === void 0 ? void 0 : _svgSymbols$get.moduleId)) { this.warn(`Symbol id "${symbolId}" already exists, will be overwritten.`); } const result = svgo.optimize(code, svgoConfig); const symbol = createSvgSymbol(result.data, symbolId); svgSymbols.set(symbolId, { moduleId, symbol }); const defaultExport = `export default ${JSON.stringify(symbolId)};`; if (!options.emitFile) { return { code: [`import { addSymbol } from "${"vite-plugin-svg-combiner"}/runtime";`, "", `addSymbol(${JSON.stringify(symbol)}, ${JSON.stringify(symbolId)});`, "", defaultExport].join("\n"), map: { mappings: "" } }; } return { code: defaultExport, map: { mappings: "" } }; }, generateBundle() { if (svgSymbols.size <= 0) return; if (options.emitFile) { this.emitFile({ type: "asset", fileName: typeof options.emitFile === "string" ? options.emitFile : defaultFileName, source: createSvgSprite([...svgSymbols.values()].map(item => item.symbol), defaultSvgSpriteId) }); } svgSymbols.clear(); } }; } module.exports = svgCombiner;