UNPKG

@endmvp/vite-plugin-svgsg

Version:

A Vite plugin to generate and manage SVG icon sprites. It scans a directory for SVG files, compiles them into a single sprite, and watches for changes to regenerate the sprite automatically.

138 lines (137 loc) 5.57 kB
import { promises as fs } from 'fs'; import * as path from 'path'; import { posix } from 'path'; import { optimize } from 'svgo'; const DEFAULT_SVGO_CONFIG = { multipass: true, plugins: [ { name: 'removeAttrs', params: { attrs: ['width', 'height', 'id'] } }, { name: 'removeStyleElement' }, { name: 'addAttributesToSVGElement', params: { attributes: [{ xmlns: 'http://www.w3.org/2000/svg' }] }, }, { name: 'removeXMLProcInst' }, { name: 'removeComments' }, { name: 'removeDoctype' }, { name: 'convertShapeToPath' }, ], js2svg: { pretty: false, indent: 0 }, }; export default function IconSpritePlugin({ iconsDir, outDir, svgoConfig = DEFAULT_SVGO_CONFIG, debounceWait = 100, }) { const logError = (message, error) => { console.error(`[IconSpritePlugin] ${message}`, error instanceof Error ? error.message : error); }; const validateDirectories = async () => { if (!iconsDir || !outDir) { throw new Error('Both iconsDir and outDir must be specified'); } const iconsDirStat = await fs.stat(iconsDir); if (!iconsDirStat.isDirectory()) { throw new Error('iconsDir must be a directory'); } const outDirStat = await fs.stat(outDir); if (!outDirStat.isDirectory()) { throw new Error('outDir must be a directory'); } }; const debounce = (func, timeout = 300) => { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => { func(...args); }, timeout); }; }; const ensureDirectoriesExist = async (iconsPath, outputPath) => { try { await fs.access(iconsPath); } catch { throw new Error(`Source directory ${iconsPath} does not exist`); } try { await fs.access(outputPath); } catch { await fs.mkdir(outputPath, { recursive: true }); console.log(`Output directory ${outputPath} created`); } }; const getSvgFiles = async (dir, baseDir) => { const entries = await fs.readdir(dir, { withFileTypes: true }); const files = []; for (const entry of entries) { const fullPath = path.join(dir, entry.name); const relativePath = path.relative(baseDir, fullPath); if (entry.isDirectory()) { files.push(...(await getSvgFiles(fullPath, baseDir))); } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.svg')) { const id = posix .normalize(relativePath) .replace(/\.svg$/i, '') .replace(/[^a-zA-Z0-9-_]/g, '_'); files.push({ id, filePath: fullPath }); console.log(`Found SVG: ${fullPath} with ID: ${id}`); } } return files; }; const optimizeSvg = async (content, id, filePath) => { const result = optimize(content, { ...svgoConfig, path: filePath }); if ('error' in result && result.error) { throw new Error(`Failed to optimize ${filePath}: ${result.error}`); } return result.data .replace(/<svg([^>]*)>/i, `<symbol id="${id}"$1>`) .replace(/<\/svg>/i, '</symbol>'); }; const generateIconSprite = async () => { try { validateDirectories(); const iconsPath = path.resolve(process.cwd(), iconsDir); const outputPath = path.resolve(process.cwd(), outDir); await ensureDirectoriesExist(iconsPath, outputPath); const files = await getSvgFiles(iconsPath, iconsPath); if (!files.length) { console.warn('[IconSpritePlugin] No SVG files found in', iconsPath); return; } const symbols = await Promise.all(files.map(async ({ id, filePath }) => { try { const svgContent = await fs.readFile(filePath, 'utf8'); return await optimizeSvg(svgContent, id, filePath); } catch (error) { logError(`Error processing ${filePath}`, error); return ''; } })); const spriteContent = `<svg width="0" height="0" style="display: none">\n${symbols.filter(Boolean).join('\n')}\n</svg>`; await fs.writeFile(path.join(outputPath, 'icon-sprite.svg'), spriteContent); console.log('Icon sprite generated successfully!'); } catch (error) { logError('Failed to generate icon sprite', error); } }; const debouncedGenerateSprite = debounce(generateIconSprite, debounceWait); return { name: 'IconSpritePlugin', buildStart() { return generateIconSprite(); }, configureServer(server) { const iconsPath = path.resolve(process.cwd(), iconsDir); server.watcher.add(path.join(iconsPath, '**/*.svg')); server.watcher.on('change', async (changedPath) => { if (changedPath.toLowerCase().endsWith('.svg') && !changedPath.includes('icon-sprite.svg')) { console.log(`SVG file changed: ${changedPath}`); await debouncedGenerateSprite(); server.ws.send({ type: 'full-reload' }); } }); }, }; }