UNPKG

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.

545 lines (537 loc) 19.1 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 = []; let didTransform = false; code = 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(...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; fs.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}__";`; } }; } export { viteAwesomeSvgLoader };