UNPKG

vite-plugin-icons-spritesheet

Version:

Vite plugin that generates a spritesheet and types out of your icons folder.

239 lines (238 loc) 7.79 kB
// src/index.ts import { promises as fs } from "node:fs"; import { mkdir } from "node:fs/promises"; import path from "node:path"; import { stderr } from "node:process"; import { Readable } from "node:stream"; import chalk from "chalk"; import { glob } from "glob"; import { parse } from "node-html-parser"; import { exec } from "tinyexec"; import { normalizePath } from "vite"; var generateIcons = async ({ withTypes = false, inputDir, outputDir, typesOutputFile = `${outputDir}/types.ts`, cwd, formatter, fileName = "sprite.svg", iconNameTransformer }) => { const cwdToUse = cwd ?? process.cwd(); const inputDirRelative = path.relative(cwdToUse, inputDir); const outputDirRelative = path.relative(cwdToUse, outputDir); const files = glob.sync("**/*.svg", { cwd: inputDir }); if (files.length === 0) { console.log(`\u26A0\uFE0F No SVG files found in ${chalk.red(inputDirRelative)}`); return; } await mkdir(outputDirRelative, { recursive: true }); await generateSvgSprite({ files, inputDir, outputPath: path.join(outputDir, fileName), outputDirRelative, iconNameTransformer, formatter }); if (withTypes) { const typesOutputDir = path.dirname(typesOutputFile); const typesFile = path.basename(typesOutputFile); const typesOutputDirRelative = path.relative(cwdToUse, typesOutputDir); await mkdir(typesOutputDirRelative, { recursive: true }); await generateTypes({ names: files.map((file) => transformIconName(file, iconNameTransformer ?? fileNameToCamelCase)), outputPath: path.join(typesOutputDir, typesFile), formatter }); } }; var transformIconName = (fileName, transformer) => { const iconName = fileName.replace(/\.svg$/, ""); return transformer(iconName); }; function fileNameToCamelCase(fileName) { const words = fileName.split("-"); const capitalizedWords = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1)); return capitalizedWords.join(""); } async function generateSvgSprite({ files, inputDir, outputPath, outputDirRelative, iconNameTransformer, formatter }) { const symbols = await Promise.all( files.map(async (file) => { const fileName = transformIconName(file, iconNameTransformer ?? fileNameToCamelCase); const input = await fs.readFile(path.join(inputDir, file), "utf8"); const root = parse(input); const svg = root.querySelector("svg"); if (!svg) { console.log(`\u26A0\uFE0F No SVG tag found in ${file}`); return; } svg.tagName = "symbol"; svg.setAttribute("id", fileName); svg.removeAttribute("xmlns"); svg.removeAttribute("xmlns:xlink"); svg.removeAttribute("version"); svg.removeAttribute("width"); svg.removeAttribute("height"); return svg.toString().trim(); }) ); const output = [ '<?xml version="1.0" encoding="UTF-8"?>', '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="0" height="0">', "<defs>", // for semantics: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs ...symbols.filter(Boolean), "</defs>", "</svg>" ].join("\n"); const formattedOutput = await lintFileContent(output, formatter, "svg"); return writeIfChanged( outputPath, formattedOutput, `\u{1F5BC}\uFE0F Generated SVG spritesheet in ${chalk.green(outputDirRelative)}` ); } async function lintFileContent(fileContent, formatter, typeOfFile) { if (!formatter) { return fileContent; } if (formatter === "biome" && typeOfFile === "svg") { return fileContent; } const prettierOptions = ["--parser", typeOfFile === "ts" ? "typescript" : "html"]; const biomeOptions = ["format", "--stdin-file-path", `file.${typeOfFile}`]; const options = formatter === "biome" ? biomeOptions : prettierOptions; const stdinStream = new Readable(); stdinStream.push(fileContent); stdinStream.push(null); const { process: process2 } = exec(formatter, options, {}); if (!process2?.stdin) { return fileContent; } stdinStream.pipe(process2.stdin); process2.stderr?.pipe(stderr); process2.on("error", (err) => { }); let formattedContent = ""; process2.stdout?.on("data", (data) => { formattedContent = formattedContent + data.toString(); }); return new Promise((resolve) => { process2.on("exit", (code) => { if (code === 0) { resolve(formattedContent); } else { resolve(fileContent); } }); }); } async function generateTypes({ names, outputPath, formatter }) { const output = [ "// This file is generated by icon spritesheet generator", "", "export const iconNames = [", ...names.map((name) => ` "${name}",`), "] as const", "", "export type IconName = typeof iconNames[number]", "" ].join("\n"); const formattedOutput = await lintFileContent(output, formatter, "ts"); const file = await writeIfChanged( outputPath, formattedOutput, `${chalk.blueBright("TS")} Generated icon types in ${chalk.green(outputPath)}` ); return file; } async function writeIfChanged(filepath, newContent, message) { try { const currentContent = await fs.readFile(filepath, "utf8"); if (currentContent !== newContent) { await fs.writeFile(filepath, newContent, "utf8"); console.log(message); } } catch (e) { await fs.writeFile(filepath, newContent, "utf8"); console.log(message); } } var iconsSpritesheet = (maybeConfigs) => { const configs = Array.isArray(maybeConfigs) ? maybeConfigs : [maybeConfigs]; const allSpriteSheetNames = configs.map((config) => config.fileName ?? "sprite.svg"); return configs.map((config, i) => { const { withTypes, inputDir, outputDir, typesOutputFile, fileName, cwd, iconNameTransformer, formatter } = config; const iconGenerator = async () => generateIcons({ withTypes, inputDir, outputDir, typesOutputFile, fileName, iconNameTransformer, formatter }); const workDir = cwd ?? process.cwd(); return { name: `icon-spritesheet-generator${i > 0 ? i.toString() : ""}`, async buildStart() { await iconGenerator(); }, async watchChange(file, type) { const inputPath = normalizePath(path.join(workDir, inputDir)); if (file.includes(inputPath) && file.endsWith(".svg") && ["create", "delete"].includes(type.event)) { await iconGenerator(); } }, async handleHotUpdate({ file }) { const inputPath = normalizePath(path.join(workDir, inputDir)); if (file.includes(inputPath) && file.endsWith(".svg")) { await iconGenerator(); } }, async config(config2) { if (i > 0) { return; } config2.build = config2.build ?? {}; const subFunc = typeof config2.build.assetsInlineLimit === "function" ? config2.build.assetsInlineLimit : void 0; const limit = typeof config2.build.assetsInlineLimit === "number" ? config2.build.assetsInlineLimit : void 0; const assetsInlineLimitFunction = (name, content) => { const isSpriteSheet = allSpriteSheetNames.some((spriteSheetName) => { return name.endsWith(normalizePath(`${outputDir}/${spriteSheetName}`)); }); if (isSpriteSheet) { return false; } if (limit) { const size = content.byteLength; return size <= limit; } if (typeof subFunc === "function") { return subFunc(name, content); } return void 0; }; config2.build.assetsInlineLimit = assetsInlineLimitFunction; } }; }); }; export { iconsSpritesheet };