Skip to content

Commit 9491b62

Browse files
Aareksioantfu
andauthoredApr 10, 2025··
feat: allow using IconifyJSON as custom collection (#366)
Co-authored-by: Anthony Fu <github@antfu.me>
1 parent 210912f commit 9491b62

File tree

8 files changed

+113
-18
lines changed

8 files changed

+113
-18
lines changed
 

‎README.md

+22
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,28 @@ Then you can use the icons like this:
181181
</template>
182182
```
183183

184+
You can also pass a full custom `IconifyJSON` object:
185+
186+
```ts
187+
export default defineNuxtConfig({
188+
modules: [
189+
'@nuxt/icon'
190+
],
191+
icon: {
192+
customCollections: [
193+
{
194+
prefix: 'paid-icons',
195+
icons: {
196+
'nuxt': { body: '<path d="M281.44 ... />' },
197+
},
198+
width: 512,
199+
height: 512,
200+
}
201+
],
202+
},
203+
})
204+
```
205+
184206
Note that custom local collections require you to have a server to serve the API. When setting `ssr: false`, the provider will default to the Iconify API (which does not have your custom icons). If you want to build a SPA with server endpoints, you can explicitly set `provider: 'server'`:
185207

186208
```ts

‎playground/components/ShowcaseFixture.vue

+9
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ defineProps<{
108108
:customize
109109
/>
110110
</p>
111+
<p>
112+
Custom icons with `prefix: 'json-collection'` from IconifyJSON:
113+
<Icon
114+
name="json-collection:nuxt-v3"
115+
size="32"
116+
:mode
117+
:customize
118+
/>
119+
</p>
111120
<p>
112121
Custom icons from layer:
113122
<Icon

‎playground/nuxt.config.ts

+10
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ export default defineNuxtConfig({
5252
dir: './icons/no-prefix',
5353
normalizeIconName: false,
5454
},
55+
{
56+
prefix: 'json-collection',
57+
icons: {
58+
'nuxt-v3': {
59+
body: '<path d="M281.44 397.667H438.32C443.326 397.667 448.118 395.908 452.453 393.427C456.789 390.946 461.258 387.831 463.76 383.533C466.262 379.236 468.002 374.36 468 369.399C467.998 364.437 466.266 359.563 463.76 355.268L357.76 172.947C355.258 168.65 352.201 165.534 347.867 163.053C343.532 160.573 337.325 158.813 332.32 158.813C327.315 158.813 322.521 160.573 318.187 163.053C313.852 165.534 310.795 168.65 308.293 172.947L281.44 219.587L227.733 129.13C225.229 124.834 222.176 120.307 217.84 117.827C213.504 115.346 208.713 115 203.707 115C198.701 115 193.909 115.346 189.573 117.827C185.238 120.307 180.771 124.834 178.267 129.13L46.8267 355.268C44.3208 359.563 44.0022 364.437 44 369.399C43.9978 374.36 44.3246 379.235 46.8267 383.533C49.3288 387.83 53.7979 390.946 58.1333 393.427C62.4688 395.908 67.2603 397.667 72.2667 397.667H171.2C210.401 397.667 238.934 380.082 258.827 346.787L306.88 263.4L332.32 219.587L410.053 352.44H306.88L281.44 397.667ZM169.787 352.44H100.533L203.707 174.36L256 263.4L221.361 323.784C208.151 345.387 193.089 352.44 169.787 352.44Z" fill="#00DC82"/>',
60+
},
61+
},
62+
width: 512,
63+
height: 512,
64+
},
5565
],
5666
serverBundle: 'remote',
5767
fallbackToApi: 'server-only',

‎playground/test/nuxt/output/fixtures.html

+19
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,25 @@
290290
</g>
291291
</svg>
292292
</p>
293+
<p>
294+
Custom icons with `prefix: 'json-collection'` from IconifyJSON:
295+
<svg
296+
xmlns="http://www.w3.org/2000/svg"
297+
xmlns:xlink="http://www.w3.org/1999/xlink"
298+
aria-hidden="true"
299+
role="img"
300+
class="iconify iconify--json-collection"
301+
style="font-size: 32px"
302+
width="1em"
303+
height="1em"
304+
viewBox="0 0 512 512"
305+
>
306+
<path
307+
d="M281.44 397.667H438.32C443.326 397.667 448.118 395.908 452.453 393.427C456.789 390.946 461.258 387.831 463.76 383.533C466.262 379.236 468.002 374.36 468 369.399C467.998 364.437 466.266 359.563 463.76 355.268L357.76 172.947C355.258 168.65 352.201 165.534 347.867 163.053C343.532 160.573 337.325 158.813 332.32 158.813C327.315 158.813 322.521 160.573 318.187 163.053C313.852 165.534 310.795 168.65 308.293 172.947L281.44 219.587L227.733 129.13C225.229 124.834 222.176 120.307 217.84 117.827C213.504 115.346 208.713 115 203.707 115C198.701 115 193.909 115.346 189.573 117.827C185.238 120.307 180.771 124.834 178.267 129.13L46.8267 355.268C44.3208 359.563 44.0022 364.437 44 369.399C43.9978 374.36 44.3246 379.235 46.8267 383.533C49.3288 387.83 53.7979 390.946 58.1333 393.427C62.4688 395.908 67.2603 397.667 72.2667 397.667H171.2C210.401 397.667 238.934 380.082 258.827 346.787L306.88 263.4L332.32 219.587L410.053 352.44H306.88L281.44 397.667ZM169.787 352.44H100.533L203.707 174.36L256 263.4L221.361 323.784C208.151 345.387 193.089 352.44 169.787 352.44Z"
308+
fill="#00DC82"
309+
></path>
310+
</svg>
311+
</p>
293312
<p>
294313
Custom icons from layer:
295314
<svg

‎src/collections.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,22 @@ export function getCollectionPath(collection: string) {
3333
// https://github.com/iconify/iconify/blob/2274c033b49c01a50dc89b490b89d803d19d95dc/packages/utils/src/icon/name.ts#L15-L18
3434
const validIconNameRE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
3535

36-
export async function loadCustomCollection(collection: CustomCollection, nuxt: Nuxt): Promise<IconifyJSON> {
36+
export async function loadCustomCollection(
37+
collection: IconifyJSON | CustomCollection,
38+
nuxt: Nuxt,
39+
): Promise<IconifyJSON> {
40+
if ('dir' in collection) {
41+
return parseCustomCollection(collection, nuxt)
42+
}
43+
44+
logger.success(`Nuxt Icon loaded local collection \`${collection.prefix}\` with ${Object.keys(collection.icons).length} icons`)
45+
return collection
46+
}
47+
48+
async function parseCustomCollection(
49+
collection: CustomCollection,
50+
nuxt: Nuxt,
51+
): Promise<IconifyJSON> {
3752
const dir = isAbsolute(collection.dir)
3853
? collection.dir
3954
: join(nuxt.options.rootDir, collection.dir)

‎src/context.ts

+35-15
Original file line numberDiff line numberDiff line change
@@ -152,20 +152,22 @@ export class NuxtIconModuleContext {
152152
// Filter out the `i-` prefix and deduplicate
153153
const userIcons = new Set((this.options.clientBundle?.icons || []).map(i => i.replace(/^i[-:]/, '')))
154154

155+
let customCollections: IconifyJSON[] = []
156+
if (this.options.customCollections?.length) {
157+
customCollections = await this.loadCustomCollection()
158+
}
159+
155160
if (scan && !this.scanner) {
156-
this.scanner = new IconUsageScanner(scan)
161+
const additionalCollections = customCollections.map(c => c.prefix)
162+
const scanOptions = scan === true ? { additionalCollections } : { additionalCollections, ...scan }
163+
this.scanner = new IconUsageScanner(scanOptions)
157164
await this.scanner.scanFiles(this.nuxt, this.scannedIcons)
158165
}
159166

160167
const icons = new Set<string>([...userIcons, ...this.scannedIcons])
161168

162169
await this.nuxt.callHook('icon:clientBundleIcons', icons)
163170

164-
let customCollections: IconifyJSON[] = []
165-
if (includeCustomCollections && this.options.customCollections?.length) {
166-
customCollections = await this.loadCustomCollection()
167-
}
168-
169171
if (!icons.size && !customCollections.length) {
170172
return {
171173
count: 0,
@@ -179,7 +181,23 @@ export class NuxtIconModuleContext {
179181
const { getIconData } = await import('@iconify/utils')
180182
const { loadCollectionFromFS } = await import('@iconify/utils/lib/loader/fs')
181183

184+
const failed: string[] = []
185+
let count = 0
186+
187+
const customCollectionNames = new Set(customCollections.map(c => c.prefix))
182188
const collections = new Map<string, IconifyJSON>()
189+
190+
function loadCollection(prefix: string) {
191+
if (customCollectionNames.has(prefix)) {
192+
const collection = customCollections.find(c => c.prefix === prefix)
193+
if (collection) {
194+
return Promise.resolve(collection)
195+
}
196+
}
197+
198+
return loadCollectionFromFS(prefix)
199+
}
200+
183201
function addIcon(prefix: string, name: string, data: IconifyIcon) {
184202
let collection = collections.get(prefix)
185203
if (!collection) {
@@ -189,17 +207,17 @@ export class NuxtIconModuleContext {
189207
}
190208
collections.set(prefix, collection)
191209
}
210+
if (!collection.icons[name]) {
211+
count += 1
212+
}
192213
collection.icons[name] = data
193214
}
194215

195-
const failed: string[] = []
196-
let count = 0
197-
198216
await Promise.all([...icons].map(async (icon) => {
199217
try {
200218
const [prefix, name] = icon.split(':')
201219
if (!iconifyCollectionMap.has(prefix))
202-
iconifyCollectionMap.set(prefix, loadCollectionFromFS(prefix))
220+
iconifyCollectionMap.set(prefix, loadCollection(prefix))
203221

204222
let data: IconifyIcon | null = null
205223
const collection = await iconifyCollectionMap.get(prefix)
@@ -213,7 +231,6 @@ export class NuxtIconModuleContext {
213231
}
214232
}
215233
else {
216-
count += 1
217234
addIcon(prefix, name, data)
218235
}
219236
}
@@ -223,10 +240,13 @@ export class NuxtIconModuleContext {
223240
}
224241
}))
225242

226-
if (customCollections.length) {
227-
customCollections.flatMap(collection => Object.entries(collection.icons)
228-
.map(([name, data]) => {
229-
addIcon(collection.prefix, name, data)
243+
if (includeCustomCollections && customCollections.length) {
244+
customCollections.flatMap(collection => Object.keys(collection.icons)
245+
.map((name) => {
246+
const data = getIconData(collection, name)
247+
if (data) {
248+
addIcon(collection.prefix, name, data)
249+
}
230250
}))
231251
}
232252

‎src/module.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ async function setupCustomCollectionsWatcher(options: ModuleOptions, nuxt: Nuxt,
182182
return
183183

184184
let viteDevServer: ViteDevServer
185-
const collectionDirs = await Promise.all(options.customCollections.map(x => nuxtResolvePath(x.dir)))
185+
const collectionDirs = await Promise.all(options.customCollections.filter(x => 'dir' in x).map(x => nuxtResolvePath(x.dir)))
186186

187187
if (options.clientBundle?.includeCustomCollections) {
188188
addVitePlugin({

‎src/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export interface ModuleOptions extends Partial<Omit<NuxtIconRuntimeOptions, 'cus
4444
/**
4545
* Custom icon collections
4646
*/
47-
customCollections?: CustomCollection[]
47+
customCollections?: (CustomCollection | IconifyJSON)[]
4848

4949
/**
5050
* List of pre-compiled CSS classnames to be used for server-side CSS icon rendering

0 commit comments

Comments
 (0)
Please sign in to comment.