mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-06-26 01:51:41 +08:00
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.
361 lines
8.7 KiB
TypeScript
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
|
|
}
|