import { join, isAbsolute, parse, normalize } from 'node:path'; import fs from 'node:fs/promises'; import { consola } from 'consola'; import { glob } from 'tinyglobby'; import { convertParsedSVG, parseSVGContent } from '@iconify/utils/lib/svg/parse'; import { isPackageExists, resolveModule } from 'local-pkg'; import pm from 'picomatch'; const collectionNames = [ "academicons", "akar-icons", "ant-design", "arcticons", "basil", "bi", "bitcoin-icons", "bpmn", "brandico", "bx", "bxl", "bxs", "bytesize", "carbon", "catppuccin", "cbi", "charm", "ci", "cib", "cif", "cil", "circle-flags", "circum", "clarity", "codex", "codicon", "covid", "cryptocurrency", "cryptocurrency-color", "cuida", "dashicons", "devicon", "devicon-plain", "dinkie-icons", "duo-icons", "ei", "el", "emojione", "emojione-monotone", "emojione-v1", "entypo", "entypo-social", "eos-icons", "ep", "et", "eva", "f7", "fa", "fa-brands", "fa-regular", "fa-solid", "fa6-brands", "fa6-regular", "fa6-solid", "fa7-brands", "fa7-regular", "fa7-solid", "fad", "famicons", "fe", "feather", "file-icons", "flag", "flagpack", "flat-color-icons", "flat-ui", "flowbite", "fluent", "fluent-color", "fluent-emoji", "fluent-emoji-flat", "fluent-emoji-high-contrast", "fluent-mdl2", "fontelico", "fontisto", "formkit", "foundation", "fxemoji", "gala", "game-icons", "garden", "geo", "gg", "gis", "gravity-ui", "gridicons", "grommet-icons", "guidance", "healthicons", "heroicons", "heroicons-outline", "heroicons-solid", "hugeicons", "humbleicons", "ic", "icomoon-free", "icon-park", "icon-park-outline", "icon-park-solid", "icon-park-twotone", "iconamoon", "iconoir", "icons8", "il", "ion", "iwwa", "ix", "jam", "la", "lets-icons", "line-md", "lineicons", "logos", "ls", "lsicon", "lucide", "lucide-lab", "mage", "majesticons", "maki", "map", "marketeq", "material-icon-theme", "material-symbols", "material-symbols-light", "mdi", "mdi-light", "medical-icon", "memory", "meteocons", "meteor-icons", "mi", "mingcute", "mono-icons", "mynaui", "nimbus", "nonicons", "noto", "noto-v1", "nrk", "octicon", "oi", "ooui", "openmoji", "oui", "pajamas", "pepicons", "pepicons-pencil", "pepicons-pop", "pepicons-print", "ph", "picon", "pixel", "pixelarticons", "prime", "proicons", "ps", "qlementine-icons", "quill", "radix-icons", "raphael", "ri", "rivet-icons", "roentgen", "si", "si-glyph", "sidekickicons", "simple-icons", "simple-line-icons", "skill-icons", "solar", "stash", "streamline", "streamline-block", "streamline-color", "streamline-cyber", "streamline-cyber-color", "streamline-emojis", "streamline-flex", "streamline-flex-color", "streamline-freehand", "streamline-freehand-color", "streamline-kameleon-color", "streamline-logos", "streamline-pixel", "streamline-plump", "streamline-plump-color", "streamline-sharp", "streamline-sharp-color", "streamline-stickies-color", "streamline-ultimate", "streamline-ultimate-color", "subway", "svg-spinners", "system-uicons", "tabler", "tdesign", "teenyicons", "temaki", "token", "token-branded", "topcoat", "twemoji", "typcn", "uil", "uim", "uis", "uit", "uiw", "unjs", "vaadin", "vs", "vscode-icons", "websymbol", "weui", "whh", "wi", "wpf", "zmdi", "zondicons" ]; const logger$1 = consola.withTag("nuxt:icon"); function hasFullCollection(resolvePaths) { return isPackageExists("@iconify/json", { paths: resolvePaths }); } async function resolveCollection(collection, rootDir) { if (typeof collection === "string") return collection; if ("dir" in collection) { return await loadCustomCollection(collection, rootDir); } return collection; } function getCollectionPath(collection, resolvePaths) { return hasFullCollection(resolvePaths) ? `@iconify/json/json/${collection}.json` : `@iconify-json/${collection}/icons.json`; } function resolveCollectionFile(collection, resolvePaths) { return resolveModule(getCollectionPath(collection, resolvePaths), { paths: resolvePaths }); } const validIconNameRE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; async function loadCustomCollection(collection, rootDir) { if ("dir" in collection) { return parseCustomCollection(collection, rootDir); } logger$1.success(`Nuxt Icon loaded local collection \`${collection.prefix}\` with ${Object.keys(collection.icons).length} icons`); return collection; } async function parseCustomCollection(collection, rootDir) { const dir = isAbsolute(collection.dir) ? collection.dir : join(rootDir, collection.dir); const { // TODO: next major flip this normalizeIconName = true, recursive = false } = collection; const pattern = recursive ? "**/*.svg" : "*.svg"; const files = (await glob([pattern], { cwd: dir, onlyFiles: true, expandDirectories: recursive })).sort(); const parsedIcons = await Promise.all(files.map(async (file) => { const { dir: path, name: filename } = parse(file); const pathNormalized = path ? normalize(path).replace(/[/\\]/g, "-") : ""; let name = pathNormalized ? `${pathNormalized}-${filename}` : filename; if (normalizeIconName && !validIconNameRE.test(name)) { const normalized = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-"); if (normalized !== name) logger$1.warn(`Custom icon \`${name}\` is normalized to \`${normalized}\`, we recommend to change the file name to match the icon name, or pass \`normalizeIconName: false\` to your custom collection options`); name = normalized; } let svg = await fs.readFile(join(dir, file), "utf-8"); const cleanupIdx = svg.indexOf(" 0) svg = svg.slice(cleanupIdx); const data = convertParsedSVG(parseSVGContent(svg)); if (!data) { logger$1.error(`Nuxt Icon could not parse the SVG content for icon \`${name}\``); return null; } if (data.top === 0) delete data.top; if (data.left === 0) delete data.left; return [name, data]; })); const successfulIcons = parsedIcons.filter((entry) => entry !== null); logger$1.success(`Nuxt Icon loaded local collection \`${collection.prefix}\` with ${successfulIcons.length} icons`); const result = { ...collection, icons: Object.fromEntries(successfulIcons) }; delete result.dir; return result; } async function discoverInstalledCollections(resolvePaths) { if (hasFullCollection(resolvePaths)) { logger$1.success(`Nuxt Icon discovered local-installed ${collectionNames.length} collections (@iconify/json)`); logger$1.warn("Currently all iconify collections are included in the bundle, which might be inefficient, consider explicit name the collections you use in the `icon.serverBundle.collections` option"); return collectionNames; } const found = /* @__PURE__ */ new Set(); if (process.versions.pnp) { for (const collection of collectionNames) { if (isPackageExists("@iconify-json/" + collection, { paths: resolvePaths })) found.add(collection); } } else { await Promise.all(iconifyScopeDirs(resolvePaths).map(async (scope) => { const entries = await fs.readdir(scope).catch(() => []); await Promise.all(entries.map(async (name) => { if (name.startsWith(".")) return; const hasIconsJson = await fs.access(join(scope, name, "icons.json")).then(() => true, () => false); if (hasIconsJson) found.add(name); })); })); } const collections = collectionNames.filter((collection) => found.has(collection)); if (collections.length) logger$1.success(`Nuxt Icon discovered local-installed ${collections.length} collections:`, collections.join(", ")); return collections; } function iconifyScopeDirs(paths) { const dirs = /* @__PURE__ */ new Set(); for (const base of paths) { let dir = base; while (true) { dirs.add(join(dir, "node_modules", "@iconify-json")); const parent = join(dir, ".."); if (parent === dir) break; dir = parent; } } return [...dirs]; } const logger = consola.withTag("nuxt:icon"); function normalizeIconName(icon) { return icon.replace(/^i[-:]/, ""); } async function resolveBundleIcons(options) { const { customCollections = [], includeCustomCollections = false, resolvePaths = [process.cwd()] } = options; const userIcons = new Set([...options.icons || []].map(normalizeIconName)); const scannedIcons = new Set([...options.scannedIcons || []].map(normalizeIconName)); const extraIcons = new Set([...options.extraIcons || []].map(normalizeIconName)); const icons = /* @__PURE__ */ new Set([...userIcons, ...scannedIcons, ...extraIcons]); if (!icons.size && !customCollections.length) { return { count: 0, collections: [], failed: [], dropped: [] }; } const iconifyCollectionMap = /* @__PURE__ */ new Map(); const { getIconData } = await import('@iconify/utils'); const { loadCollectionFromFS } = await import('@iconify/utils/lib/loader/fs'); const failed = []; const dropped = []; let count = 0; const customCollectionNames = new Set(customCollections.map((c) => c.prefix)); const collections = /* @__PURE__ */ new Map(); async function loadCollection(prefix) { if (customCollectionNames.has(prefix)) { const collection = customCollections.find((c) => c.prefix === prefix); if (collection) { return collection; } } for (const cwd of resolvePaths) { const collection = await loadCollectionFromFS(prefix, false, "@iconify-json", cwd); if (collection) { return collection; } } return void 0; } function addIcon(prefix, name, data) { let collection = collections.get(prefix); if (!collection) { collection = { prefix, icons: {} }; collections.set(prefix, collection); } if (!collection.icons[name]) { count += 1; } collection.icons[name] = data; } function markUnresolved(icon) { if (userIcons.has(icon)) { failed.push(icon); } else if (!scannedIcons.has(icon)) { dropped.push(icon); } } await Promise.all([...icons].map(async (icon) => { try { const [prefix, name] = icon.split(":"); if (prefix === void 0 || name === void 0) { throw new Error(`Invalid icon ${icon}. Expected "prefix:name" format.`); } if (!iconifyCollectionMap.has(prefix)) iconifyCollectionMap.set(prefix, loadCollection(prefix)); let data = null; const collection = await iconifyCollectionMap.get(prefix); if (collection) data = getIconData(collection, name); if (!data) { markUnresolved(icon); } else { addIcon(prefix, name, data); } } catch (e) { logger.error(e); markUnresolved(icon); } })); if (includeCustomCollections && customCollections.length) { for (const collection of customCollections) { for (const name of Object.keys(collection.icons)) { const data = getIconData(collection, name); if (data) { addIcon(collection.prefix, name, data); } } } } for (const collection of collections.values()) { const sortedEntries = Object.entries(collection.icons).sort(([nameA], [nameB]) => nameA.localeCompare(nameB)); collection.icons = Object.fromEntries(sortedEntries); } return { collections: [...collections.values()], count, failed, dropped }; } function generateClientBundleCode(collections, options = {}) { const { sizeLimitKb = 256 } = options; if (!collections.length) { return { code: "export function init() {}", bundleSizeKb: 0 }; } const valuesCompat = JSON.stringify(collections); const bundleSizeKb = Buffer.byteLength(valuesCompat, "utf-8") / 1024; if (sizeLimitKb > 0) { if (bundleSizeKb > sizeLimitKb) { throw new Error(`Nuxt Icon client bundle size limit exceeded: \`${bundleSizeKb.toFixed(2)}KB\` > \`${sizeLimitKb}KB\``); } if (bundleSizeKb > sizeLimitKb * 0.75) { logger.warn(`Nuxt Icon client bundle size is close to the limit: \`${bundleSizeKb.toFixed(2)}KB\` -> \`${sizeLimitKb}KB\``); } } const collectionsRaw = `JSON.parse(${JSON.stringify(valuesCompat)})`; const code = [ "let _initialized = false", "export function init(addIcon) {", " if (_initialized)", " return", ` const collections = ${collectionsRaw}`, ` for (const collection of collections) {`, ` for (const [name, data] of Object.entries(collection.icons)) {`, ` addIcon(collection.prefix ? (collection.prefix + ':' + name) : name, data)`, ` }`, " }", " _initialized = true", "}" ].join("\n"); return { code, bundleSizeKb }; } class IconUsageScanner { globInclude; globExclude; matchRegex; constructor(scanOptions) { const { globInclude = ["**/*.{vue,jsx,tsx,md,mdc,mdx,yml,yaml}"], globExclude = ["node_modules", "dist", "build", "coverage", "test", "tests", ".*"], ignoreCollections = [], additionalCollections = [] } = scanOptions === true ? {} : scanOptions; const collections = /* @__PURE__ */ new Set([ ...collectionNames, ...additionalCollections ]); for (const collection of ignoreCollections) { collections.delete(collection); } this.matchRegex = createMatchRegex(collections); this.globInclude = globInclude; this.globExclude = globExclude; } extractFromCode(code, set) { for (const match of code.matchAll(this.matchRegex)) { if (match) { set.add(`${match[1]}:${match[2]}`); } } } isFileMatch(path) { return pm.isMatch(path, this.globInclude) && !pm.isMatch(path, this.globExclude); } async scanFiles(cwd, set = /* @__PURE__ */ new Set()) { const dirs = Array.isArray(cwd) ? cwd : [cwd]; const files = new Set( (await Promise.all( dirs.map((dir) => glob( this.globInclude, { ignore: this.globExclude, cwd: dir, absolute: true, expandDirectories: false } )) )).flat() ); await Promise.all( [...files].map(async (file) => { const code = await fs.readFile(file, "utf-8").catch(() => ""); this.extractFromCode(code, set); }) ); return set; } } function createMatchRegex(collections) { const collectionsRegex = [...collections].sort((a, b) => b.length - a.length).join("|"); return new RegExp("\\b(?:i-)?(" + collectionsRegex + ")[:-]([a-z0-9-]+)\\b", "g"); } export { IconUsageScanner as I, createMatchRegex as a, getCollectionPath as b, collectionNames as c, discoverInstalledCollections as d, resolveCollection as e, resolveCollectionFile as f, generateClientBundleCode as g, hasFullCollection as h, loadCustomCollection as l, resolveBundleIcons as r };