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.

579 lines (570 loc) 20.9 kB
"use strict"; 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 });