Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lrstanley opened this issue Apr 23, 2022 · 8 comments
Open

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

lrstanley opened this issue Apr 23, 2022 · 8 comments

Comments

@lrstanley
Copy link

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?

@07akioni
Copy link
Owner

07akioni commented May 4, 2022

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

@lrstanley
Copy link
Author

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
Copy link
Author

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
Copy link
Author

lrstanley commented May 5, 2022

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.

@07akioni
Copy link
Owner

07akioni commented May 6, 2022

unplugin-vue-components

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

@07akioni
Copy link
Owner

07akioni commented May 6, 2022

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

@lrstanley
Copy link
Author

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.

@juanbrujo
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants