You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
307 lines
7.6 KiB
TypeScript
307 lines
7.6 KiB
TypeScript
12 months ago
|
/**
|
||
|
* 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 } from 'node:path'
|
||
|
import type { IconifyJSON, IconifyMetaData } from '@iconify/types'
|
||
|
|
||
|
// Installation: npm install --save-dev @iconify/tools @iconify/utils @iconify/json @iconify/iconify
|
||
|
import {
|
||
|
cleanupSVG,
|
||
|
importDirectory,
|
||
|
isEmptyColor,
|
||
|
parseColors,
|
||
|
runSVGO,
|
||
|
} from '@iconify/tools'
|
||
|
import { getIcons, minifyIconSet, stringToIcon } from '@iconify/utils'
|
||
|
|
||
|
import { buildUtilsReadFile, buildUtilsWriteFile } from '../../utils'
|
||
|
import {
|
||
|
IconLog,
|
||
|
SvgPrefix,
|
||
|
iconBundlePath,
|
||
|
iconListPath,
|
||
|
iconSVGPath,
|
||
|
} from '../src'
|
||
|
|
||
|
/**
|
||
|
* 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)[]
|
||
|
}
|
||
|
|
||
|
// 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 = iconBundlePath
|
||
|
|
||
|
/**
|
||
|
* Do stuff!
|
||
|
*/
|
||
|
export async function generateIconUsedBundle() {
|
||
|
const iconPools = await buildUtilsReadFile(iconListPath)
|
||
|
|
||
|
const allIconsArr = Array.from<string>(
|
||
|
JSON.parse(iconPools.replace('export default ', '')),
|
||
|
)
|
||
|
|
||
|
const sources: BundleScriptConfig = {
|
||
|
// svg: [
|
||
|
// {
|
||
|
// dir: 'svg',
|
||
|
// monotone: true,
|
||
|
// prefix: 'svg',
|
||
|
// },
|
||
|
// {
|
||
|
// dir: 'emojis',
|
||
|
// monotone: false,
|
||
|
// prefix: 'emoji',
|
||
|
// },
|
||
|
// ],
|
||
|
|
||
|
icons: allIconsArr,
|
||
|
|
||
|
// json: [
|
||
|
// // Custom JSON file
|
||
|
// 'json/gg.json',
|
||
|
// // Iconify JSON file (@iconify/json is a package name, /json/ is directory where files are, then filename)
|
||
|
// require.resolve('@iconify/json/json/tabler.json'),
|
||
|
// // Custom file with only few icons
|
||
|
// {
|
||
|
// filename: require.resolve('@iconify/json/json/line-md.json'),
|
||
|
// icons: [
|
||
|
// 'home-twotone-alt',
|
||
|
// 'github',
|
||
|
// 'document-list',
|
||
|
// 'document-code',
|
||
|
// 'image-twotone',
|
||
|
// ],
|
||
|
// },
|
||
|
// ],
|
||
|
}
|
||
|
|
||
|
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) {
|
||
|
if (prefix !== SvgPrefix) {
|
||
|
const filename = require.resolve(`@iconify/json/json/${prefix}.json`)
|
||
|
sourcesJSON.push({
|
||
|
filename,
|
||
|
icons: organizedList[prefix],
|
||
|
})
|
||
|
}
|
||
|
else {
|
||
|
sourcesJSON.push({
|
||
|
filename: iconSVGPath,
|
||
|
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 buildUtilsReadFile(filename),
|
||
|
) 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`
|
||
|
IconLog('Icon Bundle', `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
|
||
|
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 buildUtilsWriteFile(target, bundle)
|
||
|
|
||
|
IconLog(
|
||
|
'Icon Bundle',
|
||
|
`Saved bundle icons at: ${target} (${Number(bundle.length / 1024).toFixed(
|
||
|
2,
|
||
|
)} KB)`,
|
||
|
)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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
|
||
|
}
|