xicons icon indicating copy to clipboard operation
xicons copied to clipboard

feature: add support for a resolver for unplugin-auto-import

Open lrstanley opened this issue 2 years ago • 8 comments

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?

lrstanley avatar Apr 23 '22 08:04 lrstanley

I think it's possible but I am too busy to work on it.

07akioni avatar May 04 '22 20:05 07akioni

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.

lrstanley avatar May 05 '22 01:05 lrstanley

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.

lrstanley avatar May 05 '22 01:05 lrstanley

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.

lrstanley avatar May 05 '22 03:05 lrstanley

unplugin-vue-components

I think this a good example. I'll try to add it later.

07akioni avatar May 06 '22 20:05 07akioni

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).

07akioni avatar May 06 '22 20:05 07akioni

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.

lrstanley avatar May 07 '22 00:05 lrstanley

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

juanbrujo avatar Jan 03 '23 19:01 juanbrujo