Skip to content

Commit

Permalink
feat: custom loader, close #12
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Sep 14, 2021
1 parent a855a2a commit c1e2e6e
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 39 deletions.
14 changes: 11 additions & 3 deletions examples/vite-vue3/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<div class="markdown-body" style="padding: 2em 3em">
<i-logos-vue style="font-size:2em" />
<br>
<h2>Icons</h2>
<p>
Icons
<i-carbon-app-connectivity />
<i-mdi-account />
<mdi-alarm-off />
Expand All @@ -12,15 +12,23 @@
<i-ri-apps-2-line />
<i-mdi-dice-d12 />
<i-mdi-light-alarm />
<i-noto-v1-flag-for-flag-japan/>
<i-noto-v1-flag-for-flag-japan />
<i-ic-twotone-24mp />
<i-mdi:cactus />
<i-twemoji-1st-place-medal />
<IIcTwotone23mp />
<MdiStore24Hour />
<MdiAlarmOff />
from <code>unplugin-icons</code>
<br>
</p>
<h2>Custom Icons</h2>
<p>
<i-custom-steering-wheel style="color:rgb(32, 115, 129)" />
<i-custom-car-a />
<i-inline-foo />
<i-inline-async />
</p>
from <code>unplugin-icons</code>
</div>
</template>

Expand Down
4 changes: 4 additions & 0 deletions examples/vite-vue3/assets/custom-a/SteeringWheel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions examples/vite-vue3/assets/custom-a/car-a.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/vite-vue3/assets/giftbox.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions examples/vite-vue3/components.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/vue-next/pull/3399

declare module 'vue' {
export interface GlobalComponents {
ICarbonAppConnectivity: typeof import('~icons/carbon/app-connectivity')['default']
ICustomCarA: typeof import('~icons/custom/car-a')['default']
ICustomSteeringWheel: typeof import('~icons/custom/steering-wheel')['default']
IFaSolidDiceFive: typeof import('~icons/fa-solid/dice-five')['default']
IHeroiconsOutlineMenuAlt2: typeof import('~icons/heroicons-outline/menu-alt2')['default']
IIcTwotone23mp: typeof import('~icons/ic/twotone23mp')['default']
IIcTwotone24mp: typeof import('~icons/ic/twotone24mp')['default']
IInlineAsync: typeof import('~icons/inline/async')['default']
IInlineFoo: typeof import('~icons/inline/foo')['default']
ILogosVue: typeof import('~icons/logos/vue')['default']
'IMdi:cactus': typeof import('~icons/mdi/cactus')['default']
IMdiAccount: typeof import('~icons/mdi/account')['default']
IMdiDiceD12: typeof import('~icons/mdi/dice-d12')['default']
IMdiLightAlarm: typeof import('~icons/mdi-light/alarm')['default']
INotoV1FlagForFlagJapan: typeof import('~icons/noto-v1/flag-for-flag-japan')['default']
IRiApps2Line: typeof import('~icons/ri/apps2-line')['default']
ITwemoji1stPlaceMedal: typeof import('~icons/twemoji/1st-place-medal')['default']
}
}

export { }
4 changes: 4 additions & 0 deletions examples/vite-vue3/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')
19 changes: 15 additions & 4 deletions examples/vite-vue3/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,32 @@
import { promises as fs } from 'fs'
import { UserConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import Icons from 'unplugin-icons/vite'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
import Components from 'unplugin-vue-components/vite'

const config: UserConfig = {
plugins: [
Vue(),
Icons({
compiler: 'vue3',
customCollections: {
custom: FileSystemIconLoader('assets/custom-a'),
inline: {
foo: '<svg width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 100 100"><clipPath id="IconifyId-17be3d363e5-c8de42-104"><circle cx="50" cy="50" r="50"></circle></clipPath><g fill-rule="evenodd" clip-rule="evenodd" clip-path="url(#IconifyId-17be3d363e5-c8de42-104)"><circle fill="#316EAC" cx="50" cy="50" r="50"></circle><path fill="#fff" d="M14.084 107.072a2 2 0 0 1-2-1.977L12.023 100H7a2 2 0 0 1-2-2V78c0-.323.078-.642.229-.928l11-21c.16-.306.396-.564.685-.751l16.375-10.596L40.08 21.44a2 2 0 0 1 1.617-1.417L42 20a2 2 0 0 1 1.664.891l5.62 8.429l5.349 1.783c.452.151.837.459 1.082.869l13.971 23.285l8.285-4.971a1.997 1.997 0 0 1 2.629.514l12 16c.083.11.154.229.213.354l7 15a2.002 2.002 0 0 1-.599 2.436l-28.916 22.072c-.349.266-.775.41-1.214.41h-55z"></path><path fill="#fff" d="M69.084 105.073h-55L14 98H7V78l11-21l17-11l7-24l6 9l6 2l15 25l10-6l12 16l7 15l-28.916 22.073z"></path><path fill="#6BC8F2" d="M8 108a2 2 0 0 1-2-2V76a2 2 0 0 1 .211-.895l10-20a1.99 1.99 0 0 1 .703-.784l16.328-10.565l5.813-24.222A2 2 0 0 1 41 18h.039a2 2 0 0 1 1.923 1.607l3 15c.038.196.048.395.028.593l-1 10a2.012 2.012 0 0 1-.111.484l-4 11l-4.81 13.468l2.891 14.456c.068.342.046.695-.063 1.025L35 97.324V106a2 2 0 0 1-2 2H8zm62.998-25c-.337 0-.678-.085-.99-.264l-7-4a2.001 2.001 0 0 1-.924-2.311l6-20c.15-.5.489-.921.944-1.174l9-5a2.014 2.014 0 0 1 1.987.025c.61.36.985 1.015.985 1.724v17c0 .395-.117.781-.336 1.109l-8 12a2 2 0 0 1-1.666.891z"></path><path fill="#6BC8F2" d="M70 57l-6 20l7 4l8-12V52l-9 5zM35 45L18 56L8 76v30h25v-9l4-12l-3-15l5-14l4-11l1-10l-3-15l-6 25z"></path><circle opacity=".2" fill="#fff" cx="76.5" cy="29.5" r="1.5"></circle><circle opacity=".1" fill="#fff" cx="14.5" cy="40.5" r="1.5"></circle><circle opacity=".43" fill="#fff" cx="56.5" cy="15.5" r="1.5"></circle></g></svg>',
async: () => fs.readFile('assets/giftbox.svg', 'utf-8'),
},
},
}),
Components({
dts: true,
resolvers: [
IconsResolver(),
IconsResolver({
customCollections: ['custom', 'inline'],
}),
],
}),
Icons({
compiler: 'vue3',
}),
],
}

Expand Down
1 change: 1 addition & 0 deletions loaders.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dist/loaders'
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"import": "./dist/index.mjs"
},
"./*": "./*",
"./loaders": {
"require": "./dist/loaders.js",
"import": "./dist/loaders.mjs"
},
"./nuxt": {
"require": "./dist/nuxt.js",
"import": "./dist/nuxt.mjs"
Expand Down Expand Up @@ -51,15 +55,16 @@
"*.d.ts"
],
"scripts": {
"build": "rimraf dist && tsup src/*.ts --format cjs,esm --dts --external vue && esno scripts/postbuild.ts",
"dev": "tsup src/*.ts --format cjs,esm --watch src --external vue",
"build": "tsup src/*.ts --clean --format cjs,esm --dts --external vue && esno scripts/postbuild.ts",
"dev": "tsup src/*.ts --clean --format cjs,esm --dts --watch src --external vue",
"example:build": "npm -C examples/vue3 run build",
"example:dev": "npm -C examples/vue3 run dev",
"lint": "eslint --ext .ts,.js,.vue",
"prepublishOnly": "npm run build",
"release": "npx bumpp --commit --tag && npm publish && git push"
},
"dependencies": {
"@antfu/utils": "^0.3.0",
"@iconify/json-tools": "^1.0.10",
"has-pkg": "^0.0.1",
"unplugin": "^0.2.9"
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 44 additions & 16 deletions src/core/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,45 +68,73 @@ export function getCollection(name: string) {
return _collections[name]
}

export function getIcon(name: string, icon: string) {
const collection = getCollection(name)
if (!collection)
export function getBuiltinIcon(collection: string, icon: string) {
const icons = getCollection(collection)
if (!icons)
return null

let data: any
for (const trans of _idTransforms) {
data = collection.getIconData(trans(icon))
data = icons.getIconData(trans(icon))
if (data)
return data
}
return null
}

export async function generateComponent({ collection, icon }: ResolvedIconPath, options: ResolvedOptions) {
const data = getIcon(collection, icon)
if (!data)
throw new Error(`Icon \`${collection}:${icon}\` not found`)
export async function getIcon(collection: string, icon: string, options: ResolvedOptions) {
const { scale } = options

const custom = options.customCollections[collection]

if (custom) {
let result: string | undefined | null

if (typeof custom === 'function') {
result = await custom(icon)
}
else {
const inline = custom[icon]
result = typeof inline === 'function'
? await inline()
: inline
}

if (result) {
if (!result.startsWith('<svg '))
console.warn(`Custom icon "${icon}" in "${collection}" is not a valid SVG`)
return result.replace('<svg ', `<svg height="${scale}em" width="${scale}em" `)
}
}

const { scale, defaultStyle, defaultClass } = options
const svg = new SVG(data)
let svgText: string = svg.getSVG({
const iconData = getBuiltinIcon(collection, icon)

const svg = new SVG(iconData)
const svgText: string = svg.getSVG({
height: `${scale}em`,
width: `${scale}em`,
})

if (!svgText)
return null
return svgText
}

export async function generateComponent({ collection, icon }: ResolvedIconPath, options: ResolvedOptions) {
let svg = await getIcon(collection, icon, options)
if (!svg)
throw new Error(`Icon \`${collection}:${icon}\` not found`)

const { defaultStyle, defaultClass } = options

if (defaultClass)
svgText = svgText.replace('<svg ', `<svg class="${defaultClass}" `)
svg = svg.replace('<svg ', `<svg class="${defaultClass}" `)
if (defaultStyle)
svgText = svgText.replace('<svg ', `<svg style="${defaultStyle}" `)
svg = svg.replace('<svg ', `<svg style="${defaultStyle}" `)

const compiler = compilers[options.compiler]
if (!compiler)
throw new Error(`Unknown compiler: ${options.compiler}`)

return compiler(svgText, collection, icon, options)
return compiler(svg, collection, icon, options)
}

export async function generateComponentFromPath(path: string, options: ResolvedOptions) {
Expand Down
2 changes: 2 additions & 0 deletions src/core/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export function resolveOptions(options: Options): ResolvedOptions {
defaultClass = '',
compiler = guessCompiler(),
jsx = guessJSX(),
customCollections = {},
} = options

const webComponents = Object.assign({
Expand All @@ -19,6 +20,7 @@ export function resolveOptions(options: Options): ResolvedOptions {
scale,
defaultStyle,
defaultClass,
customCollections,
compiler,
jsx,
webComponents,
Expand Down
13 changes: 13 additions & 0 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
export function camelize(str: string) {
return str.replace(/-([a-z0-9])/g, g => g[1].toUpperCase())
}

export function pascalize(str: string) {
const camel = camelize(str)
return camel[0].toUpperCase() + camel.slice(1)
}

export function camelToKebab(key: string) {
const result = key
.replace(/:/g, '-')
.replace(/([A-Z])/g, ' $1')
.trim()
return result.split(/\s+/g).join('-').toLowerCase()
}
17 changes: 17 additions & 0 deletions src/loaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { existsSync, promises as fs } from 'fs'
import { camelize, pascalize } from './core/utils'
import { CustomIconLoader } from '.'

export function FileSystemIconLoader(dir: string): CustomIconLoader {
return (name) => {
const pathes = [
`${dir}/${name}.svg`,
`${dir}/${camelize(name)}.svg`,
`${dir}/${pascalize(name)}.svg`,
]
for (const path of pathes) {
if (existsSync(path))
return fs.readFile(path, 'utf-8')
}
}
}
Loading

0 comments on commit c1e2e6e

Please sign in to comment.