'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 ``;
}
/**
* 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(/", "");
}
/**
* 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;