vite-awesome-svg-loader
Version:
Imports SVGs as source code, base64 and data URI. Preserves stroke width, replaces colors with currentColor. Optimizes SVGs with SVGO. Creates SVG sprites.
579 lines (570 loc) • 20.9 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
viteAwesomeSvgLoader: () => viteAwesomeSvgLoader
});
module.exports = __toCommonJS(index_exports);
// src/loader.ts
var import_fs_extra = __toESM(require("fs-extra"));
var import_path2 = __toESM(require("path"));
var import_svgo = require("svgo");
var import_xast2 = require("svgo/lib/xast.js");
var import_imurmurhash = __toESM(require("imurmurhash"));
// src/internal/misc.ts
var import_xast = require("svgo/lib/xast.js");
var import_path = __toESM(require("path"));
function normalizeBaseDir(dir) {
dir = dir.replaceAll("\\", "/");
if (dir.endsWith("/")) {
dir = dir.substring(0, dir.length - 1);
}
return dir;
}
function toBase64(str) {
const binString = String.fromCodePoint(...new TextEncoder().encode(str));
return btoa(binString);
}
function escapeBackticks(str) {
return str.replaceAll("`", "\\`");
}
function matchesQueryOrList(relPathWithSlash, queryValue, matchers) {
return matchesQuery(queryValue) || matchesPath(relPathWithSlash, matchers);
}
function matchesQuery(queryValue) {
return !!queryValue && queryValue.toLowerCase() !== "false";
}
function matchesPath(relPathWithSlash, matchers) {
const filename = import_path.default.basename(relPathWithSlash);
const toMatch = [filename, relPathWithSlash];
for (const matcher of matchers) {
const isRegex = matcher instanceof RegExp;
for (const entry of toMatch) {
const matches = isRegex ? matcher.test(entry) : entry === matcher;
if (matches) {
return true;
}
}
}
return false;
}
function normalizeSelector(selector) {
return selector.replaceAll(/\s+/g, " ").trim();
}
function selectorsToList(relPathWithSlash, selectors, returnEmptyList) {
const resolvedSelectors = [];
if (returnEmptyList) {
return resolvedSelectors;
}
for (const selector of selectors) {
if (typeof selector === "string") {
resolvedSelectors.push(normalizeSelector(selector));
continue;
}
if (matchesPath(relPathWithSlash, selector.files)) {
for (const selectorStr of selector.selectors) {
resolvedSelectors.push(normalizeSelector(selectorStr));
}
}
}
return resolvedSelectors;
}
var matchesSelector = import_xast.matches;
function matchesSelectors(node, selectors) {
for (const selector of selectors) {
if (matchesSelector(node, selector)) {
return true;
}
}
return false;
}
function replaceColor(color, replacements) {
if (!color) {
return replacements.default || "";
}
return replacements.replacements[color.toLowerCase()] || replacements.default || color;
}
// src/internal/preserveLineWidth.ts
var TAGS_TO_PRESERVE_LINE_WIDTH_OF = {
circle: true,
ellipse: true,
foreignObject: true,
image: true,
line: true,
path: true,
polygon: true,
polyline: true,
rect: true,
text: true,
textPath: true,
tspan: true,
use: true
};
function preserveLineWidth(node, path3) {
if (!TAGS_TO_PRESERVE_LINE_WIDTH_OF[node.name]) {
return;
}
const vectorEffectAttr = node.attributes["vector-effect"];
if (vectorEffectAttr && vectorEffectAttr !== "non-scaling-stroke") {
console.warn(
`"${path3}": Element "${node.name}" already contains "vector-effect" property. Please remove it, so it can scale correctly. This element will not be transformed.`
);
} else {
node.attributes["vector-effect"] = "non-scaling-stroke";
}
}
// src/internal/const.ts
var COLOR_ATTRS_TO_REPLACE = {
"fill": true,
"stroke": true,
"stop-color": true
};
var IGNORE_COLORS = {
none: true,
transparent: true,
currentColor: true
};
var IMPORT_TYPES = ["url", "source", "source-data-uri", "base64", "base64-data-uri"];
// src/internal/replaceColorsCss.ts
var csstree = __toESM(require("css-tree"));
function replaceColorsCss(css, replacements, nodesWithOrigColors, isInline = false) {
if (!css || typeof css !== "string") {
return "";
}
let context = "stylesheet";
if (isInline) {
css = `{${css}}`;
context = "block";
}
const shouldPreserveColors = !isInline && nodesWithOrigColors.length;
let origColorSelectors = [];
let currentColorSelectors = [];
let didSplitSelectors = false;
const ast = csstree.parse(css, { context });
csstree.walk(ast, {
// Ignore because of broken types in csstree:
// @ts-ignore
visit: shouldPreserveColors ? void 0 : "Declaration",
enter: function(node) {
var _a, _b, _c, _d, _e, _f, _g;
if (node.__SKIP_SVG_LOADER__ || ((_a = this.rule) == null ? void 0 : _a.__SKIP_SVG_LOADER__)) {
return;
}
if (shouldPreserveColors) {
if (node.type === "SelectorList") {
origColorSelectors = [];
currentColorSelectors = [];
didSplitSelectors = false;
return;
}
if (node.type === "Selector") {
const selector = csstree.generate(node);
let isOrigColor = false;
for (const svgNode of nodesWithOrigColors) {
if (matchesSelector(svgNode, selector)) {
isOrigColor = true;
node.__ORIG_COLOR__ = true;
break;
}
}
(isOrigColor ? origColorSelectors : currentColorSelectors).push(selector);
return;
}
}
if (node.type !== "Declaration" || !COLOR_ATTRS_TO_REPLACE[node.property]) {
return;
}
const identifier = (_c = (_b = node.value) == null ? void 0 : _b.children) == null ? void 0 : _c.first;
const color = (identifier == null ? void 0 : identifier.value) || (identifier == null ? void 0 : identifier.name);
if (!color || IGNORE_COLORS[color]) {
return;
}
if (shouldPreserveColors && !didSplitSelectors && ((_d = this.rule) == null ? void 0 : _d.prelude.type) === "SelectorList") {
const origColorsRule = csstree.clone(this.rule);
origColorsRule.__SKIP_SVG_LOADER__ = true;
const origColorsSelectors = new csstree.List();
const selectors = this.rule.prelude.children;
selectors.forEach((node2, listItem) => {
if (node2.__ORIG_COLOR__) {
selectors.remove(listItem);
origColorsSelectors.push(node2);
}
});
origColorsRule.prelude.children = origColorsSelectors;
const parent = ((_f = (_e = this.atrule) == null ? void 0 : _e.block) == null ? void 0 : _f.children) || ((_g = this.stylesheet) == null ? void 0 : _g.children);
let insertBefore;
parent == null ? void 0 : parent.some((rule, listItem) => {
if (rule === this.rule) {
insertBefore = listItem;
return true;
}
return false;
});
insertBefore ? parent == null ? void 0 : parent.insertData(origColorsRule, insertBefore) : parent == null ? void 0 : parent.push(origColorsRule);
didSplitSelectors = true;
}
node.value = csstree.parse(replaceColor(csstree.generate(node.value), replacements), {
context: "value"
});
}
});
return csstree.generate(ast);
}
// src/internal/replaceColorsSvg.ts
var ELEMENTS_TO_FORCE_SET_FILL_OF = {
circle: true,
ellipse: true,
path: true,
polygon: true,
polyline: true,
rect: true,
text: true,
textPath: true,
tref: true,
tspan: true
};
var COLOR_ATTRS_TO_REPLACE_SVG = __spreadValues({}, COLOR_ATTRS_TO_REPLACE);
delete COLOR_ATTRS_TO_REPLACE_SVG.fill;
function replaceColorsSvg(node, isFillSetOnRoot, replacements, nodesWithOrigColors) {
if (node.name === "style") {
const firstChild = node.children[0];
const newCss = replaceColorsCss(firstChild == null ? void 0 : firstChild.value, replacements, nodesWithOrigColors, false);
if (newCss) {
firstChild.value = newCss;
}
} else {
const newCss = replaceColorsCss(node.attributes.style, replacements, nodesWithOrigColors, true);
if (newCss) {
node.attributes.style = newCss;
}
}
const isRoot = node.name === "svg";
const fillAttr = node.attributes.fill;
if (isRoot && fillAttr) {
isFillSetOnRoot = true;
}
if ((isRoot && isFillSetOnRoot || !isRoot && !isFillSetOnRoot && ELEMENTS_TO_FORCE_SET_FILL_OF[node.name]) && !IGNORE_COLORS[fillAttr]) {
node.attributes.fill = replaceColor(fillAttr, replacements);
}
for (const attr in COLOR_ATTRS_TO_REPLACE_SVG) {
const attrsColor = node.attributes[attr];
if (attrsColor && !IGNORE_COLORS[attrsColor]) {
node.attributes[attr] = replaceColor(attrsColor, replacements);
}
}
return isFillSetOnRoot;
}
// src/loader.ts
var DEFAULT_OPTIONS = {
tempDir: ".temp",
preserveLineWidthList: [],
skipPreserveLineWidthList: [],
skipPreserveLineWidthSelectors: [],
setCurrentColorList: [],
skipSetCurrentColorList: [],
skipSetCurrentColorSelectors: [],
replaceColorsList: [],
skipReplaceColorsList: [],
skipReplaceColorsSelectors: [],
skipTransformsList: [],
skipTransformsSelectors: [],
skipFilesList: [],
defaultImport: "source"
};
function viteAwesomeSvgLoader(options = {}) {
const mergedOptions = __spreadValues(__spreadValues({}, DEFAULT_OPTIONS), options);
mergedOptions.tempDir = mergedOptions.tempDir.replaceAll("\\", "/");
if (mergedOptions.tempDir.startsWith("/") || mergedOptions.tempDir.startsWith("./") || mergedOptions.tempDir.indexOf(":/") !== -1) {
throw new Error(
`"tempDir" option must be in format "path/to/temp/dir",i.e. it shouldn't be an absolute path, or start with "./".It'll be resolved to the project's root by the plugin.`
);
}
if (mergedOptions.tempDir.endsWith("/")) {
mergedOptions.tempDir = mergedOptions.tempDir.substring(0, mergedOptions.tempDir.length - 1);
}
mergedOptions.tempDir = "/" + mergedOptions.tempDir;
let isBuildMode = false;
let root = "";
let base = "";
let oldViteRoot = "";
const replaceColorsList = options.setCurrentColorList || mergedOptions.replaceColorsList;
const replacementsWithFiles = [];
const filesWithCurrentColor = [];
const allFilesReplacements = {
files: [/.*/],
replacements: {},
default: ""
};
let hasAllFilesReplacements = false;
for (const replacements of replaceColorsList) {
if (typeof replacements === "string" || replacements instanceof RegExp) {
filesWithCurrentColor.push({
files: [replacements],
replacements: {},
default: "currentColor"
});
continue;
}
if (replacements.files instanceof Array) {
replacementsWithFiles.push(replacements);
continue;
}
for (const color in replacements) {
hasAllFilesReplacements = true;
allFilesReplacements.replacements[color] = replacements[color];
}
}
const replaceColorsListNormalized = [...replacementsWithFiles, ...filesWithCurrentColor];
if (hasAllFilesReplacements) {
replaceColorsListNormalized.push(allFilesReplacements);
}
return {
name: "vite-awesome-svg-loader",
enforce: "pre",
config(config, { command }) {
isBuildMode = command === "build";
},
configResolved(config) {
root = normalizeBaseDir(config.root);
base = normalizeBaseDir(config.base);
oldViteRoot = root[1] === ":" ? root.substring(2) : root;
},
configureServer(server) {
var _a;
(_a = server.httpServer) == null ? void 0 : _a.on("close", () => {
if (!isBuildMode) {
import_fs_extra.default.removeSync(root + mergedOptions.tempDir);
}
});
},
resolveId(source, importer) {
if (source.indexOf(".svg") === -1) {
return null;
}
if (source.startsWith(oldViteRoot)) {
return root + source.substring(oldViteRoot.length);
}
if (!source.startsWith(".")) {
return source;
}
if (!importer) {
return null;
}
return import_path2.default.join(import_path2.default.dirname(importer), source);
},
load(id) {
var _a, _b;
const ext = ".svg";
const indexOfSvg = id.indexOf(ext);
if (indexOfSvg === -1) {
return null;
}
let relPathWithSlash = id.substring(0, indexOfSvg + ext.length).replaceAll("\\", "/");
if (relPathWithSlash.startsWith(root)) {
relPathWithSlash = relPathWithSlash.substring(root.length);
}
if (!relPathWithSlash.startsWith("/")) {
relPathWithSlash = "/" + relPathWithSlash;
}
const queryStr = id.split("?", 2)[1] || "";
const queryKVPairs = queryStr.split("&");
const query = {};
for (const pair of queryKVPairs) {
const [key, value] = pair.split("=");
query[key.toLowerCase()] = value || "1";
}
if (matchesQueryOrList(relPathWithSlash, query["skip-awesome-svg-loader"], mergedOptions.skipFilesList)) {
return null;
}
const shouldSkipTransforms = matchesQueryOrList(
relPathWithSlash,
query["skip-transforms"],
mergedOptions.skipTransformsList
);
const shouldPreserveLineWidth = !shouldSkipTransforms && matchesQueryOrList(relPathWithSlash, query["preserve-line-width"], mergedOptions.preserveLineWidthList) && !matchesQueryOrList(relPathWithSlash, void 0, mergedOptions.skipPreserveLineWidthList);
const skipPreserveLineWidthSelectors = selectorsToList(
relPathWithSlash,
mergedOptions.skipPreserveLineWidthSelectors,
!shouldPreserveLineWidth
);
let shouldReplaceColors = false;
const colorReplacements = {
replacements: {},
// @ts-ignore
default: void 0
};
if (!shouldSkipTransforms && !matchesQueryOrList(
relPathWithSlash,
void 0,
options.skipSetCurrentColorList || mergedOptions.skipReplaceColorsList
)) {
if (matchesQuery(query["set-current-color"])) {
colorReplacements.default = "currentColor";
shouldReplaceColors = true;
} else {
for (const replacements of replaceColorsListNormalized) {
if (!matchesPath(relPathWithSlash, replacements.files)) {
continue;
}
shouldReplaceColors = true;
if (colorReplacements.default === void 0 && replacements.default !== void 0) {
colorReplacements.default = replacements.default;
}
for (const color in replacements.replacements) {
(_a = colorReplacements.replacements)[color] || (_a[color] = replacements.replacements[color]);
}
}
}
}
(_b = colorReplacements.default) != null ? _b : colorReplacements.default = "currentColor";
const skipReplaceColorsSelectors = selectorsToList(
relPathWithSlash,
options.skipSetCurrentColorSelectors || mergedOptions.skipReplaceColorsSelectors,
!shouldReplaceColors
);
const skipTransformsSelectors = selectorsToList(
relPathWithSlash,
mergedOptions.skipTransformsSelectors,
shouldSkipTransforms
);
const hashParts = [relPathWithSlash];
for (const arr of [skipPreserveLineWidthSelectors, skipReplaceColorsSelectors, skipTransformsSelectors]) {
hashParts.push(arr.join(","));
}
for (const param of [shouldSkipTransforms, shouldPreserveLineWidth, shouldReplaceColors]) {
hashParts.push(param ? "1" : "0");
}
if (shouldReplaceColors) {
hashParts.push(JSON.stringify(colorReplacements));
}
const hash = new import_imurmurhash.default(hashParts.join("__")).result();
const fileNameNoExt = import_path2.default.basename(relPathWithSlash).split(".")[0];
const assetFileNameNoExt = `${fileNameNoExt}-${hash}`;
const assetFileName = assetFileNameNoExt + ".svg";
const assetRelPath = import_path2.default.dirname(relPathWithSlash) + "/" + assetFileName;
const fullPath = root + relPathWithSlash;
let code = import_fs_extra.default.readFileSync(fullPath).toString();
let isFillSetOnRoot = false;
const nodesWithOrigColors = [];
let didTransform = false;
code = (0, import_svgo.optimize)(code, {
multipass: true,
plugins: [
{
name: "prefixIds",
params: {
prefixIds: true,
prefixClassNames: true,
prefix: assetFileNameNoExt
}
},
{
name: "awesome-svg-loader",
fn: () => {
if (didTransform) {
return null;
}
didTransform = true;
return {
root: {
enter: (root2) => {
for (const selectors of [skipReplaceColorsSelectors, skipTransformsSelectors]) {
for (const selector of selectors) {
nodesWithOrigColors.push(...(0, import_xast2.querySelectorAll)(root2, selector));
}
}
}
},
element: {
enter: (node) => {
if (matchesSelectors(node, skipTransformsSelectors)) {
return;
}
if (shouldPreserveLineWidth && !matchesSelectors(node, skipPreserveLineWidthSelectors)) {
preserveLineWidth(node, fullPath);
}
if (shouldReplaceColors && !matchesSelectors(node, skipReplaceColorsSelectors)) {
isFillSetOnRoot = replaceColorsSvg(node, isFillSetOnRoot, colorReplacements, nodesWithOrigColors);
}
}
}
};
}
}
]
}).data;
let importType = mergedOptions.defaultImport;
for (const type of IMPORT_TYPES) {
if (query[type]) {
importType = type;
}
}
switch (importType) {
case "source":
return "export default `" + escapeBackticks(code) + "`;";
case "source-data-uri":
return "export default `data:image/svg+xml," + encodeURIComponent(code) + "`;";
case "base64":
return "export default `" + escapeBackticks(toBase64(code)) + "`;";
case "base64-data-uri":
return "export default `data:image/svg+xml;base64," + encodeURIComponent(toBase64(code)) + "`;";
}
if (!isBuildMode) {
const assetUrl = mergedOptions.tempDir + assetRelPath;
import_fs_extra.default.outputFileSync(root + assetUrl, code);
return `export default "${base + assetUrl}"`;
}
const assetId = this.emitFile({
type: "asset",
name: assetFileName,
source: code
});
return `export default "__VITE_ASSET__${assetId}__";`;
}
};
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
viteAwesomeSvgLoader
});