diff --git a/packages/svg-sprites/README.md b/packages/svg-sprites/README.md index 1bfbde9c5d..eddc9d48d0 100644 --- a/packages/svg-sprites/README.md +++ b/packages/svg-sprites/README.md @@ -39,6 +39,15 @@ export const Icon = (props: IconProps) => { }; ``` +The package also emits grouped sprite files following the local bundle format, for example: + +```text +fluent_icons_20_regular.sprite.svg +fluent_icons_20_filled.sprite.svg +``` + +Those files contain all symbols for a given size/style pairing, with ids like `access_time`, `add`, and `alert` instead of repeating the size/style suffix inside each id. + ## Development ### Building Sprites diff --git a/packages/svg-sprites/build-verify.test.js b/packages/svg-sprites/build-verify.test.js index 8fe121e87d..619956da00 100644 --- a/packages/svg-sprites/build-verify.test.js +++ b/packages/svg-sprites/build-verify.test.js @@ -48,17 +48,34 @@ describe('Build Verification', () => { // Sprite should be a valid SVG expect(content, `${file} should contain with matching id const expectedId = path.basename(file, '.sprite.svg'); - expect(content, `${file} should contain with id="${expectedId}"`).toMatch( - new RegExp(`]+id="${expectedId}"`), - ); + const isGroupedSprite = /^fluent_icons_\d+_(regular|filled|light|color)\.sprite\.svg$/.test(file); + + if (isGroupedSprite) { + expect(content, `${file} should contain elements`).toMatch(/ with matching id + expect(content, `${file} should contain with id="${expectedId}"`).toMatch( + new RegExp(`]+id="${expectedId}"`), + ); + } // Symbol should have viewBox expect(content, `${file} symbol should have viewBox`).toMatch(/]+viewBox="/); } }); + it('should group all icons for a size and style into one sprite', async () => { + const file = 'fluent_icons_20_regular.sprite.svg'; + const content = await readFile(path.join(SPRITES_DIR, file), 'utf8'); + const symbolIds = [...content.matchAll(/]+id="([^"]+)"/g)].map((match) => match[1]); + + expect(symbolIds.length, `${file} should contain multiple symbols`).toBeGreaterThan(1); + expect(new Set(symbolIds).size, `${file} should contain unique symbol ids`).toBe(symbolIds.length); + expect(symbolIds).toContain('access_time'); + expect(symbolIds).toContain('add'); + }); + // TODO: to enable this we would need to update snapshot during release - lets avoid that for now it.skip('should have a stable set of sprite files (snapshot)', async () => { const entries = await readdir(SPRITES_DIR); diff --git a/packages/svg-sprites/generate-sprites.js b/packages/svg-sprites/generate-sprites.js index 4307c87c20..33d198297b 100755 --- a/packages/svg-sprites/generate-sprites.js +++ b/packages/svg-sprites/generate-sprites.js @@ -49,6 +49,43 @@ function processArgs() { return { ICONS_DIR, SPRITES_DIR, NUM_WORKERS }; } +/** + * Parses an icon filename into its semantic id + variant metadata. + * @param {string} fileName + * @returns {{ iconId: string, size?: string, style?: string, fileName: string }} + */ +function parseIconMeta(fileName) { + const withoutExt = path.basename(fileName, '.svg'); + const match = withoutExt.match(/^(.*)_(\d+)_(regular|filled|light|color)$/); + + if (match) { + return { + iconId: match[1], + size: match[2], + style: match[3], + fileName: withoutExt, + }; + } + + return { iconId: withoutExt, fileName: withoutExt }; +} + +/** + * Builds a combined sprite file containing multiple symbols. + * @param {{ iconId: string, iconPath: string }[]} entries + * @returns {Promise} + */ +async function createCombinedSprite(entries) { + const sprites = svgstore(); + + for (const entry of entries) { + const iconContent = await fs.readFile(entry.iconPath, 'utf-8'); + sprites.add(entry.iconId, iconContent); + } + + return sprites.toString(); +} + /** * Creates a sprite SVG file from a single icon SVG using svgstore * @param {string} iconPath - Path to the icon file @@ -140,7 +177,7 @@ async function main() { console.log(`šŸ“Š Processing ${svgFiles.length} icons with ${NUM_WORKERS} workers...`); - // Split work into batches (one per CPU core) + // Build the existing one-icon-per-file sprite set. const batchSize = Math.ceil(svgFiles.length / NUM_WORKERS); const batches = []; @@ -152,18 +189,45 @@ async function main() { } } - // Process all batches in parallel const results = await Promise.all(batches); - - // Flatten results const allResults = results.flat(); const successful = allResults.filter((r) => r.success).length; const failed = allResults.filter((r) => !r.success); + // Also generate combined files grouped by size and style, matching the local bundle layout. + const groupedBySizeAndStyle = new Map(); + + for (const file of svgFiles) { + const meta = parseIconMeta(file); + if (!meta.size || !meta.style) { + continue; + } + + const key = `${meta.size}_${meta.style}`; + const bucket = groupedBySizeAndStyle.get(key) ?? []; + bucket.push({ + iconId: meta.iconId, + iconPath: path.join(ICONS_DIR, file), + }); + groupedBySizeAndStyle.set(key, bucket); + } + + const combinedFiles = []; + + for (const [key, entries] of groupedBySizeAndStyle) { + const spriteContent = await createCombinedSprite(entries); + const outputFile = `fluent_icons_${key}.sprite.svg`; + const outputPath = path.join(SPRITES_DIR, outputFile); + await fs.writeFile(outputPath, spriteContent, 'utf-8'); + combinedFiles.push(outputFile); + } + const duration = ((Date.now() - startTime) / 1000).toFixed(2); const durationNum = parseFloat(duration); - console.log(`\nāœ… Generated ${successful} sprites in ${duration}s`); + console.log( + `\nāœ… Generated ${successful} per-icon sprites and ${combinedFiles.length} grouped sprites in ${duration}s`, + ); console.log(`⚔ Performance: ${(svgFiles.length / durationNum).toFixed(0)} sprites/second`); if (failed.length > 0) { diff --git a/packages/svg-sprites/package.json b/packages/svg-sprites/package.json index 28092c08de..f9c4196da8 100644 --- a/packages/svg-sprites/package.json +++ b/packages/svg-sprites/package.json @@ -17,7 +17,7 @@ "optimize": "yarn run -T svgo --config svgo.config.js --folder=./icons --precision=2", "unfill": "node unfill.js --path=./icons/", "sprites": "node generate-sprites.js && rm -rf ./icons", - "build": "yarn copy && yarn rename && yarn unfill && yarn optimize && yarn sprites", + "build": "yarn clean && yarn copy && yarn rename && yarn unfill && yarn optimize && yarn sprites", "build-verify": "yarn run -T vitest run build-verify.test.js" } }