Files
MoviePilot-Frontend/src/@iconify/build-icons.ts
jxxghp dbeea6afcc perf: reduce frontend memory pressure and startup cost
Limit long-lived page and component retention while virtualizing large card views to keep runtime memory lower. Defer heavy editor, chart, workflow, calendar, and icon code so the app loads less JavaScript up front.
2026-05-09 08:32:14 +08:00

361 lines
8.7 KiB
TypeScript

/**
* This is an advanced example for creating icon bundles for Iconify SVG Framework.
*
* It creates a bundle from:
* - All SVG files in a directory.
* - Custom JSON files.
* - Iconify icon sets.
* - SVG framework.
*
* This example uses Iconify Tools to import and clean up icons.
* For Iconify Tools documentation visit https://docs.iconify.design/tools/tools2/
*/
import { promises as fs } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
// Get current directory
const __dirname = dirname(fileURLToPath(import.meta.url))
const projectSrcDir = join(__dirname, '..')
// Create require function for importing JSON files in ESM
const require = createRequire(import.meta.url)
// Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify
import {
cleanupSVG,
importDirectory,
isEmptyColor,
parseColors,
runSVGO,
} from '@iconify/tools'
import type { IconifyJSON, IconifyMetaData } from '@iconify/types'
import { getIcons, minifyIconSet, stringToIcon } from '@iconify/utils'
/**
* Script configuration
*/
interface BundleScriptCustomSVGConfig {
// Path to SVG files
dir: string
// True if icons should be treated as monotone: colors replaced with currentColor
monotone: boolean
// Icon set prefix
prefix: string
}
interface BundleScriptCustomJSONConfig {
// Path to JSON file
filename: string
// List of icons to import. If missing, all icons will be imported
icons?: string[]
}
interface BundleScriptConfig {
// Custom SVG to import and bundle
svg?: BundleScriptCustomSVGConfig[]
// Icons to bundled from @iconify/json packages
icons?: string[]
// List of JSON files to bundled
// Entry can be a string, pointing to filename or a BundleScriptCustomJSONConfig object (see type above)
// If entry is a string or object without 'icons' property, an entire JSON file will be bundled
json?: (string | BundleScriptCustomJSONConfig)[]
}
const sources: BundleScriptConfig = {
svg: [
// {
// dir: 'src/assets/images/iconify-svg',
// monotone: true,
// prefix: 'custom',
// },
// {
// dir: 'emojis',
// monotone: false,
// prefix: 'emoji',
// },
],
icons: [
'lucide:sparkles',
'material-symbols:passkey',
'line-md:loading-twotone-loop',
],
json: [],
}
// Iconify component (this changes import statement in generated file)
// Available options: '@iconify/react' for React, '@iconify/vue' for Vue 3, '@iconify/vue2' for Vue 2, '@iconify/svelte' for Svelte
const component = '@iconify/vue'
// Set to true to use require() instead of import
const commonJS = false
// File to save bundle to
const target = join(__dirname, 'icons-bundle.js');
/**
* Do stuff!
*/
// eslint-disable-next-line sonarjs/cognitive-complexity
(async function () {
const scannedIcons = await collectUsedIcons(projectSrcDir)
if (sources.icons) {
sources.icons.push(...scannedIcons)
sources.icons = Array.from(new Set(sources.icons)).sort()
} else {
sources.icons = scannedIcons
}
let bundle = commonJS
? `const { addCollection } = require('${component}');\n\n`
: `import { addCollection } from '${component}';\n\n`
// Create directory for output if missing
const dir = dirname(target)
try {
await fs.mkdir(dir, {
recursive: true,
})
}
catch (err) {
//
}
/**
* Convert sources.icons to sources.json
*/
if (sources.icons) {
const sourcesJSON = sources.json ? sources.json : (sources.json = [])
// Sort icons by prefix
const organizedList = organizeIconsList(sources.icons)
for (const prefix in organizedList) {
let filename
try {
filename = require.resolve(`@iconify-json/${prefix}/icons.json`)
}
catch (err) {
filename = require.resolve(`@iconify/json/json/${prefix}.json`)
}
sourcesJSON.push({
filename,
icons: organizedList[prefix],
})
}
}
/**
* Bundle JSON files
*/
if (sources.json) {
for (let i = 0; i < sources.json.length; i++) {
const item = sources.json[i]
// Load icon set
const filename = typeof item === 'string' ? item : item.filename
let content = JSON.parse(
await fs.readFile(filename, 'utf8'),
) as IconifyJSON
// Filter icons
if (typeof item !== 'string' && item.icons?.length) {
const filteredContent = getIcons(content, item.icons)
if (!filteredContent)
throw new Error(`Cannot find required icons in ${filename}`)
content = filteredContent
}
// Remove metadata and add to bundle
removeMetaData(content)
minifyIconSet(content)
bundle += `addCollection(${JSON.stringify(content)});\n`
console.log(`Bundled icons from ${filename}`)
}
}
/**
* Custom SVG
*/
if (sources.svg) {
for (let i = 0; i < sources.svg.length; i++) {
const source = sources.svg[i]
// Import icons
const iconSet = await importDirectory(source.dir, {
prefix: source.prefix,
})
// Validate, clean up, fix palette and optimise
await iconSet.forEach(async (name, type) => {
if (type !== 'icon')
return
// Get SVG instance for parsing
const svg = iconSet.toSVG(name)
if (!svg) {
// Invalid icon
iconSet.remove(name)
return
}
// Clean up and optimise icons
try {
// Clean up icon code
await cleanupSVG(svg)
if (source.monotone) {
// Replace color with currentColor, add if missing
// If icon is not monotone, remove this code
await parseColors(svg, {
defaultColor: 'currentColor',
callback: (attr, colorStr, color) => {
return (!color || isEmptyColor(color))
? colorStr
: 'currentColor'
},
})
}
// Optimise
await runSVGO(svg)
}
catch (err) {
// Invalid icon
console.error(
`Error parsing ${name} from ${source.dir}:`,
err,
)
iconSet.remove(name)
return
}
// Update icon from SVG instance
iconSet.fromSVG(name, svg)
})
console.log(`Bundled ${iconSet.count()} icons from ${source.dir}`)
// Export to JSON
const content = iconSet.export()
bundle += `addCollection(${JSON.stringify(content)});\n`
}
}
// Save to file
await fs.writeFile(target, bundle, 'utf8')
console.log(`Saved ${target} (${bundle.length} bytes)`)
})().catch((err) => {
console.error(err)
})
async function collectUsedIcons(rootDir: string): Promise<string[]> {
const icons = new Set<string>()
const files = await walkDirectory(rootDir)
const sourceFiles = files.filter(file => /\.(vue|ts|js|tsx|jsx)$/.test(file))
for (const file of sourceFiles) {
if (file.includes('/@iconify/')) {
continue
}
const content = await fs.readFile(file, 'utf8')
for (const match of content.matchAll(/\b(lucide|material-symbols|line-md|tabler):([a-z0-9-]+)\b/g)) {
icons.add(`${match[1]}:${match[2]}`)
}
for (const match of content.matchAll(/\bmdi:([a-z0-9-]+)\b/g)) {
icons.add(`mdi:${match[1]}`)
}
for (const match of content.matchAll(/\btabler-([a-z0-9-]+)\b/g)) {
icons.add(`tabler:${match[1]}`)
}
for (const match of content.matchAll(/\bmdi-([a-z0-9-]+)\b/g)) {
icons.add(`mdi:${match[1]}`)
}
}
return Array.from(icons).sort()
}
async function walkDirectory(dir: string): Promise<string[]> {
const entries = await fs.readdir(dir, { withFileTypes: true })
const files: string[] = []
for (const entry of entries) {
const fullPath = join(dir, entry.name)
if (entry.isDirectory()) {
files.push(...(await walkDirectory(fullPath)))
continue
}
files.push(fullPath)
}
return files
}
/**
* Remove metadata from icon set
*/
function removeMetaData(iconSet: IconifyJSON) {
const props: (keyof IconifyMetaData)[] = [
'info',
'chars',
'categories',
'themes',
'prefixes',
'suffixes',
]
props.forEach((prop) => {
delete iconSet[prop]
})
}
/**
* Sort icon names by prefix
*/
function organizeIconsList(icons: string[]): Record<string, string[]> {
const sorted: Record<string, string[]> = Object.create(null)
icons.forEach((icon) => {
const item = stringToIcon(icon)
if (!item)
return
const prefix = item.prefix
const prefixList = sorted[prefix]
? sorted[prefix]
: (sorted[prefix] = [])
const name = item.name
if (!prefixList.includes(name))
prefixList.push(name)
})
return sorted
}