Skip to content

Commit

Permalink
feat: add dynamicIconsPlugin
Browse files Browse the repository at this point in the history
  • Loading branch information
hyoban committed Dec 16, 2023
1 parent 88a31cd commit b53dde9
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 36 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,27 @@ module.exports = {

Then you can use this custom icon as class name: `i-foo-arrow-left`.

### Generate Icon Dynamically

The idea is from [@iconify/tailwind](https://iconify.design/docs/usage/css/tailwind),
thanks to the author of Iconify for the great work!

If you want to install `@iconify/json` and use whatever icon you want,
you should add another plugin to your `tailwind.config.js`.

This is because we can not provide autocomplete for all icons from `@iconify/json`,
it will make your editor slow.

```js
const { iconsPlugin, dynamicIconsPlugin } = require("@egoist/tailwindcss-icons")

module.exports = {
plugins: [iconsPlugin(), dynamicIconsPlugin()],
}
```

Then you can use icons dynamically like `<span class="i-[mdi-light--home]"></span>`.

## Sponsors

[![sponsors](https://sponsors-images.egoist.dev/sponsors.svg)](https://github.com/sponsors/egoist)
Expand Down
8 changes: 4 additions & 4 deletions example/tailwind.config.cjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { iconsPlugin, dynamicIconsPlugin } = require("../dist");

module.exports = {
content: ["./index.html"],
plugins: [
require("../dist").iconsPlugin(),
],
}
plugins: [iconsPlugin(), dynamicIconsPlugin()],
};
23 changes: 17 additions & 6 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,21 @@ import { IconifyIcon, IconifyJSON } from "@iconify/types"
import { getIconCSS, getIconData } from "@iconify/utils"
import { createRequire } from "module"
import { CollectionNames } from "../types"
import { GenerateOptions } from "./types"

export type GenerateOptions = {
/**
* Scale relative to the current font size (1em).
*
* @default 1
*/
scale?: number
/**
* Extra CSS properties applied to the generated CSS.
*
* @default `{}`
*/
extraProperties?: Record<string, string>
}

declare const TSUP_FORMAT: "esm" | "cjs"
const req =
Expand Down Expand Up @@ -53,7 +67,7 @@ export const isPackageExists = (id: string) => {
}

export const getIconCollections = (
include: CollectionNames[] | "all" = "all",
include: CollectionNames[],
): Record<string, IconifyJSON> => {
const p = callerPath()
const cwd = p ? path.dirname(p) : process.cwd()
Expand All @@ -80,10 +94,7 @@ export const getIconCollections = (
const files = fs.readdirSync(path.join(pkgDir, "json"))
const collections: Record<string, IconifyJSON> = {}
for (const file of files) {
if (
include === "all" ||
include.includes(file.replace(".json", "") as any)
) {
if (include.includes(file.replace(".json", "") as any)) {
const json: IconifyJSON = req(path.join(pkgDir, "json", file))
collections[json.prefix] = json
}
Expand Down
48 changes: 48 additions & 0 deletions src/dynamic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { IconifyJSON } from "@iconify/types"
import { type CollectionNames, availableCollectionNames } from "../types"
import {
generateComponent,
getIconCollections,
type GenerateOptions,
} from "./core"

const cache = new Map<CollectionNames, IconifyJSON>()

function getIconCollection(name: CollectionNames) {
const cached = cache.get(name)
if (cached) return cached

const collection = getIconCollections([name])[name]
if (collection) cache.set(name, collection)
return collection
}

export function getDynamicCSSRules(
icon: string,
options: GenerateOptions,
): Record<string, string> {
const nameParts = icon.split(/--|\:/)
if (nameParts.length !== 2) {
throw new Error(`Invalid icon name: "${icon}"`)
}

const [prefix, name] = nameParts
if (!availableCollectionNames.includes(prefix as CollectionNames)) {
throw new Error(`Invalid collection name: "${prefix}"`)
}

const icons = getIconCollection(prefix as CollectionNames)

const generated = generateComponent(
{
icons,
name,
},
options,
)
if (!generated) {
throw new Error(`Invalid icon name: "${icon}"`)
}

return generated
}
50 changes: 49 additions & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { test, expect } from "vitest"
import postcss from "postcss"
import tailwindcss from "tailwindcss"
import { getIconCollections, iconsPlugin } from "."
import { dynamicIconsPlugin, getIconCollections, iconsPlugin } from "."

test("main", async () => {
const result = await postcss([
Expand Down Expand Up @@ -292,3 +292,51 @@ test("custom icon collection name", async () => {
}"
`)
})

test("generate icon dynamically", async () => {
const result = await postcss([
tailwindcss({
config: {
content: [
{
raw: '<span class="i-heroicons-archive-box"></span>',
extension: "html",
},
],
plugins: [iconsPlugin(), dynamicIconsPlugin()],
},
}),
]).process(`@tailwind components;
.foo {
@apply i-[mdi--home];
}`)

expect(result.css).toMatchInlineSnapshot(`
".i-heroicons-archive-box {
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
--svg: url(\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' stroke='black' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m20.25 7.5l-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125'/%3E%3C/svg%3E\\")
}
.foo {
display: inline-block;
width: 1em;
height: 1em;
background-color: currentColor;
-webkit-mask-image: var(--svg);
mask-image: var(--svg);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
--svg: url(\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M10 20v-6h4v6h5v-8h3L12 3L2 12h3v8z'/%3E%3C/svg%3E\\")
}"
`)
})
31 changes: 29 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { IconifyJSONIconsData } from "@iconify/types"
import plugin from "tailwindcss/plugin.js"
import { parseIconSet } from "@iconify/utils"
import {
type GenerateOptions,
generateIconComponent,
getIconCollections,
isPackageExists,
} from "./core"
import { getDynamicCSSRules } from "./dynamic"
import { CollectionNames, availableCollectionNames } from "../types"
import { type Optional } from "./utils"
import { IconsOptions } from "./types"

export { getIconCollections, type CollectionNames }

Expand All @@ -23,7 +24,13 @@ export type IconsPluginOptions = {
* @default {}
*/
collectionNamesAlias?: CollectionNamesAlias
} & IconsOptions
/**
* Class prefix for matching icon rules.
*
* @default `i`
*/
prefix?: string
} & GenerateOptions

export const iconsPlugin = (iconsPluginOptions?: IconsPluginOptions) => {
const {
Expand Down Expand Up @@ -73,3 +80,23 @@ export const iconsPlugin = (iconsPluginOptions?: IconsPluginOptions) => {
)
})
}

export const dynamicIconsPlugin = (
iconsPluginOptions?: Omit<
IconsPluginOptions,
"collections" | "collectionNamesAlias"
>,
) => {
const {
prefix = "i",
scale = 1,
extraProperties = {},
} = iconsPluginOptions ?? {}

return plugin(({ matchComponents }) => {
matchComponents({
[prefix]: (value) =>
getDynamicCSSRules(value, { scale, extraProperties }),
})
})
}
23 changes: 0 additions & 23 deletions src/types.ts

This file was deleted.

0 comments on commit b53dde9

Please sign in to comment.