diff --git a/packages/icons-webfont/.build/build-outline.mjs b/packages/icons-webfont/.build/build-outline.mjs index a178ee41f..dc3da535b 100644 --- a/packages/icons-webfont/.build/build-outline.mjs +++ b/packages/icons-webfont/.build/build-outline.mjs @@ -6,9 +6,26 @@ import crypto from 'crypto' import { glob } from 'glob' import { optimize } from 'svgo' import { fixOutline } from './fix-outline.mjs' +import os from 'os' const DIR = getPackageDir('icons-webfont') +// Parallel processing with concurrency limit +const parallelLimit = async (items, fn, concurrency = os.cpus().length * 2) => { + const results = [] + let index = 0 + + const worker = async () => { + while (index < items.length) { + const i = index++ + results[i] = await fn(items[i], i) + } + } + + await Promise.all(Array(Math.min(concurrency, items.length)).fill(null).map(worker)) + return results +} + const strokes = { 200: 1, 300: 1.5, @@ -24,79 +41,81 @@ const buildOutline = async () => { for (const strokeName in strokes) { const stroke = strokes[strokeName] - await asyncForEach(Object.entries(icons), async ([type, icons]) => { + for (const [type, typeIcons] of Object.entries(icons)) { fs.mkdirSync(resolve(DIR, `icons-outlined/${strokeName}/${type}`), { recursive: true }) - filesList[type] = [] - - await asyncForEach(icons, async function ({ name, content, unicode }) { - if (compileOptions.includeIcons.length === 0 || compileOptions.includeIcons.indexOf(name) >= 0) { - - if (unicode) { - console.log(`Stroke ${strokeName} for:`, name, unicode) - - let filename = `${name}.svg` - if (unicode) { - filename = `u${unicode.toUpperCase()}-${name}.svg` - } - - filesList[type].push(filename) - - content = content - .replace('width="24"', 'width="1000"') - .replace('height="24"', 'height="1000"') - - content = content - .replace('stroke-width="2"', `stroke-width="${stroke}"`) - - const cachedFilename = `u${unicode.toUpperCase()}-${name}.svg`; - - if (unicode && fs.existsSync(resolve(DIR, `icons-outlined/${strokeName}/${type}/${cachedFilename}`))) { - // Get content - let cachedContent = fs.readFileSync(resolve(DIR, `icons-outlined/${strokeName}/${type}/${cachedFilename}`), 'utf-8') - - // Get hash - let cachedHash = ''; - cachedContent = cachedContent.replace(//, function (m, hash) { - cachedHash = hash; - return ''; - }) - - // Check hash - if (crypto.createHash('sha1').update(cachedContent).digest("hex") === cachedHash) { - console.log('Cached stroke for:', name, unicode) - return true; - } - } - - await outlineStroke(content, { - optCurve: true, - steps: 4, - round: 0, - centerHorizontally: true, - fixedWidth: false, - color: 'black' - }).then(outlined => { - // Fix outline direction (using JS instead of fontforge) - const fixed = fixOutline(outlined) - - // Optimize with svgo (in memory, no subprocess) - const optimized = optimize(fixed, { multipass: true }).data - - // Prepare final content with hash - const finalContent = optimized.replace(/\n/g, ' ').trim() - const hashString = `` - - // Save file (single write instead of 3 file operations) - fs.writeFileSync( - resolve(DIR, `icons-outlined/${strokeName}/${type}/${filename}`), - finalContent + hashString, - 'utf-8' - ) - }).catch(error => console.log(error)) + + // Filter icons first + const iconsToProcess = typeIcons.filter(({ name, unicode }) => { + if (!unicode) return false + if (compileOptions.includeIcons.length > 0 && compileOptions.includeIcons.indexOf(name) < 0) return false + return true + }) + + // Collect filenames for later cleanup + filesList[type] = iconsToProcess.map(({ name, unicode }) => `u${unicode.toUpperCase()}-${name}.svg`) + + // Process icons in parallel with concurrency limit + let processed = 0 + const total = iconsToProcess.length + + await parallelLimit(iconsToProcess, async ({ name, content, unicode }) => { + const filename = `u${unicode.toUpperCase()}-${name}.svg` + const filePath = resolve(DIR, `icons-outlined/${strokeName}/${type}/${filename}`) + + // Check cache + if (fs.existsSync(filePath)) { + let cachedContent = fs.readFileSync(filePath, 'utf-8') + let cachedHash = '' + cachedContent = cachedContent.replace(//, (m, hash) => { + cachedHash = hash + return '' + }) + + if (crypto.createHash('sha1').update(cachedContent).digest("hex") === cachedHash) { + processed++ + process.stdout.write(`\rStroke ${strokeName}/${type}: ${processed}/${total} (cached: ${name})`.padEnd(80)) + return } } - }) - }) + + // Prepare content + content = content + .replace('width="24"', 'width="1000"') + .replace('height="24"', 'height="1000"') + .replace('stroke-width="2"', `stroke-width="${stroke}"`) + + try { + const outlined = await outlineStroke(content, { + optCurve: true, + steps: 4, + round: 0, + centerHorizontally: true, + fixedWidth: false, + color: 'black' + }) + + // Fix outline direction (using JS instead of fontforge) + const fixed = fixOutline(outlined) + + // Optimize with svgo (in memory, no subprocess) + const optimized = optimize(fixed, { multipass: true }).data + + // Prepare final content with hash + const finalContent = optimized.replace(/\n/g, ' ').trim() + const hashString = `` + + // Save file + fs.writeFileSync(filePath, finalContent + hashString, 'utf-8') + + processed++ + process.stdout.write(`\rStroke ${strokeName}/${type}: ${processed}/${total} (${name})`.padEnd(80)) + } catch (error) { + console.error(`\nError processing ${name}:`, error.message) + } + }, 32) // 32 concurrent tasks + + console.log() // New line after progress + } // Remove old files await asyncForEach(Object.entries(icons), async ([type, icons]) => {