UNPKG

vite-awesome-svg-loader

Version:

Imports SVGs as source code, base64 and data URI. Preserves stroke width, replaces colors with currentColor or custom colors. Optimizes SVGs with SVGO. Creates SVG sprites.

554 lines (546 loc) 19.4 kB
var __defProp = Object.defineProperty; var __getOwnPropSymbols = Object.getOwnPropertySymbols; 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; }; // src/loader.ts import fs from "fs-extra"; import path2 from "path"; import { optimize } from "svgo"; import { querySelectorAll } from "svgo/lib/xast.js"; import MurmurHash3 from "imurmurhash"; // src/internal/misc.ts import { matches as matchesSelectorRaw } from "svgo/lib/xast.js"; import path from "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 = path.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 = matchesSelectorRaw; 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 import * as csstree from "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) { fs.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 path2.join(path2.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 MurmurHash3(hashParts.join("__")).result(); const fileNameNoExt = path2.basename(relPathWithSlash).split(".")[0]; const assetFileNameNoExt = `${fileNameNoExt}-${hash}`; const assetFileName = assetFileNameNoExt + ".svg"; const assetRelPath = path2.dirname(relPathWithSlash) + "/" + assetFileName; const fullPath = root + relPathWithSlash; let code = fs.readFileSync(fullPath).toString(); let isFillSetOnRoot = false; const nodesWithOrigColors = []; const classesAndIdsPrefix = assetFileNameNoExt + "__"; let didTransform = false; code = optimize(code, { multipass: true, plugins: [ { name: "prefixIds", params: { prefixIds: true, prefixClassNames: true, prefix: classesAndIdsPrefix, delim: "" } }, { 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(...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; } } const getExports = (src) => { return [ `export const src = ${src};`, `export const prefix = "${classesAndIdsPrefix}"`, `export default src` ].join("\n"); }; switch (importType) { case "source": return getExports("`" + escapeBackticks(code) + "`"); case "source-data-uri": return getExports("`data:image/svg+xml," + encodeURIComponent(code) + "`"); case "base64": return getExports("`" + escapeBackticks(toBase64(code)) + "`"); case "base64-data-uri": return getExports("`data:image/svg+xml;base64," + encodeURIComponent(toBase64(code)) + "`"); } if (!isBuildMode) { const assetUrl = mergedOptions.tempDir + assetRelPath; fs.outputFileSync(root + assetUrl, code); return getExports(`"${base + assetUrl}"`); } const assetId = this.emitFile({ type: "asset", name: assetFileName, source: code }); return getExports(`"__VITE_ASSET__${assetId}__"`); } }; } export { viteAwesomeSvgLoader };