xicons
xicons copied to clipboard
feature: add support for a resolver for unplugin-auto-import
Given the current nature of how many icons exist within each sub-package, it'd be nice to have support to auto-import icons using antfu/unplugin-auto-import.
Normally it'd be pretty easy to add a custom resolver, however due to the icons not having naming prefixes, my guess is the best bet is generating a resolver for each package, that includes the list of importable icons.
The benefit in doing so, if folks use unplugin-auto-import
, is that they no longer need to import any icons, and only the referenced icons are actually bundled.
Thoughts?
I think it's possible but I am too busy to work on it.
I was going to submit a PR, however the repo structure is a bit confusing to me, so I'll just add the items that I was able to get working, here.
This would add the unplugin-auto-import
functionality, which should work regardless of frontend package defined within xicons:
// icon-resolver.ts
import { readdirSync } from 'fs'
import { dirname } from 'path'
import { resolveModule } from 'local-pkg'
import type { ImportsMap } from 'unplugin-auto-import/types'
let _cache: ImportsMap | undefined
export default (pkg: string): ImportsMap => {
if (!_cache) {
let packages: Array<string>
try {
const icon_path = resolveModule(pkg) as string
packages = readdirSync(dirname(icon_path), { withFileTypes: true })
.filter(item => !item.isDirectory() && item.name.match(/^[A-Z][A-Za-z0-9]+\.js$/))
.map(item => item.name.replace(/\.js$/, ''))
}
catch (error) {
console.error(error)
throw new Error(`[auto-import] failed to load "${pkg}", have you installed it?`)
}
if (packages) {
_cache = {
[pkg]: packages,
}
}
}
return _cache || {}
}
And this would be how it can be used:
// vite.config.js
import { defineConfig } from "vite"
import AutoImport from "unplugin-auto-import/vite"
import IconResolver from "./icon-resolver.ts"
export default defineConfig({
// [...]
plugins: [
// [...]
AutoImport({
imports: [
// [...]
// replacing @vicons/ionicons5 with whatever package the user is using.
// if it's not installed, it will throw an exception, suggesting to install it.
// can also be specified multiple times to load multiple packages.
IconResolver("@vicons/ionicons5"),
],
}),
],
})
Then, you can just use FingerPrint
for example, in any of your related components, and don't have to import it manually.
Also going to work on a unplugin-vue-components
resolver, if you'd be interested in that -- there might be other versions that can be added in a similar fashion as the Vue one as well.
That would allow referencing the icons as components dynamically, as well. Not just JS/TS.
And here is the vue auto-component-import resolver:
// icon-component-resolver.ts
import { readdirSync } from 'fs'
import { dirname } from 'path'
import { resolveModule } from 'local-pkg'
import type { ComponentResolver } from 'unplugin-vue-components/types'
let _cache: Array<string>
export interface IconResolverOptions {
pkg: string
prefix?: string
}
export function IconComponentResolver(options: IconResolverOptions): ComponentResolver {
if (!_cache) {
try {
const icon_path = resolveModule(options.pkg) as string
_cache = readdirSync(dirname(icon_path), { withFileTypes: true })
.filter(item => !item.isDirectory() && item.name.match(/^[A-Z][A-Za-z0-9]+\.js$/))
.map(item => item.name.replace(/\.js$/, ''))
} catch (error) {
console.error(error)
throw new Error(`[unplugin-vue-components] failed to load "${options.pkg}", have you installed it?`)
}
}
return {
type: 'component',
resolve: (name: string) => {
if (name.startsWith(options.prefix)) {
name = name.substring(options.prefix.length)
}
if (_cache.includes(name)) {
return {
name: name,
from: options.pkg,
}
}
},
}
}
And usage example:
// vite.config.js
import { defineConfig } from "vite"
import Components from "unplugin-vue-components/vite"
import { IconComponentResolver } from "./icon-component-resolver.ts"
export default defineConfig({
// [...]
plugins: [
// [...]
Components({
directoryAsNamespace: true,
resolvers: [IconComponentResolver({ pkg: "@vicons/ionicons5", prefix: "X" })],
}),
],
})
As shown above, you can supply an optional prefix, primarily as a way of ensuring that the components can follow the standard of requiring a dash (e.g. <XGithubIcon />
, <x-github-icon />
or whatever the user sets the prefix to), as there are some icons which wouldn't follow that standard (At
for ionicons5 is one example, <At />
is not spec compliant).
With the above, a user just dropping in two lines into their vite.config.js
(or similar), they can reference icons, on-demand, without having to import them, in their vue components.
unplugin-vue-components
I think this a good example. I'll try to add it later.
Also I've a question. Is there any performance issue? since there may be more than 5k icon files in a single package (fluent icons, meterial icons).
I haven't tried fluent or material yet, but I did try ionicons5 with aliased import names, which would have been near that amount, and it was still very fast, adding virtually no overhead. With that being said, I also do have a Ryzen 9 5900x and 32GB ram, so I'm not sure on a slower device, what it'd look like.
It should still be very fast I'd think though, as both of the above approaches gather the list of importable names and returns those, and that array is cached until you restart Vite (or update any of the resolver/vite files). It would only be iterating through a relatively lightweight array, and if you use one of the matching names, only then does it actually import it (so still tree shakeable, like normal). Vites internal cache I believe also means that it's not going to re-import each HMR, similar to any other module import.
And here is the vue auto-component-import resolver:
// icon-component-resolver.ts import { readdirSync } from 'fs' import { dirname } from 'path' import { resolveModule } from 'local-pkg' import type { ComponentResolver } from 'unplugin-vue-components/types' let _cache: Array<string> export interface IconResolverOptions { pkg: string prefix?: string } export function IconComponentResolver(options: IconResolverOptions): ComponentResolver { if (!_cache) { try { const icon_path = resolveModule(options.pkg) as string _cache = readdirSync(dirname(icon_path), { withFileTypes: true }) .filter(item => !item.isDirectory() && item.name.match(/^[A-Z][A-Za-z0-9]+\.js$/)) .map(item => item.name.replace(/\.js$/, '')) } catch (error) { console.error(error) throw new Error(`[unplugin-vue-components] failed to load "${options.pkg}", have you installed it?`) } } return { type: 'component', resolve: (name: string) => { if (name.startsWith(options.prefix)) { name = name.substring(options.prefix.length) } if (_cache.includes(name)) { return { name: name, from: options.pkg, } } }, } }
And usage example:
// vite.config.js import { defineConfig } from "vite" import Components from "unplugin-vue-components/vite" import { IconComponentResolver } from "./icon-component-resolver.ts" export default defineConfig({ // [...] plugins: [ // [...] Components({ directoryAsNamespace: true, resolvers: [IconComponentResolver({ pkg: "@vicons/ionicons5", prefix: "X" })], }), ], })
As shown above, you can supply an optional prefix, primarily as a way of ensuring that the components can follow the standard of requiring a dash (e.g.
<XGithubIcon />
,<x-github-icon />
or whatever the user sets the prefix to), as there are some icons which wouldn't follow that standard (At
for ionicons5 is one example,<At />
is not spec compliant).With the above, a user just dropping in two lines into their
vite.config.js
(or similar), they can reference icons, on-demand, without having to import them, in their vue components.
great catch, this worked perfect with Vite + Vue 3 and Antdv 3