Skip to content

Commit 3aec879

Browse files
committed
feat(vite): add HMR support for local SVG
1 parent b4d7fca commit 3aec879

4 files changed

Lines changed: 99 additions & 1 deletion

File tree

examples/vite-vue3/vite.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ const config: UserConfig = {
4545
props.color = 'skyblue'
4646
}
4747
},
48+
hmrResolver(file, folder, normalizedSVGIconName) {
49+
console.log(file, folder, normalizedSVGIconName)
50+
if (file.endsWith('assets/giftbox.svg')) {
51+
return 'inline/async'
52+
}
53+
if (folder.endsWith('assets/custom-a')) {
54+
return `custom/${normalizedSVGIconName}`
55+
}
56+
},
4857
}),
4958
Components({
5059
dts: true,

src/core/options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export async function resolveOptions(options: Options): Promise<ResolvedOptions>
1717
transform,
1818
autoInstall = false,
1919
collectionsNodeResolvePath = process.cwd(),
20+
hmrResolver,
2021
} = options
2122

2223
const webComponents = Object.assign({
@@ -38,6 +39,7 @@ export async function resolveOptions(options: Options): Promise<ResolvedOptions>
3839
transform,
3940
autoInstall,
4041
collectionsNodeResolvePath,
42+
hmrResolver,
4143
}
4244
}
4345

src/index.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { Options } from './types'
2+
import { basename, dirname } from 'node:path'
3+
import { camelToKebab } from '@iconify/utils/lib/misc/strings'
24
import { createUnplugin } from 'unplugin'
35
import { generateComponentFromPath, isIconPath, normalizeIconPath, resolveIconsPath } from './core/loader'
46
import { resolveOptions } from './core/options'
@@ -58,6 +60,43 @@ const unplugin = createUnplugin<Options | undefined>((options = {}) => {
5860
}
5961
}
6062
},
63+
vite: {
64+
async handleHotUpdate({ file, server }) {
65+
const hmrResolver = await resolved.then(({ hmrResolver }) => hmrResolver)
66+
if (!hmrResolver) {
67+
return undefined
68+
}
69+
const iconId = await hmrResolver(
70+
file,
71+
dirname(file).replace(/\\/g, '/'),
72+
camelToKebab(basename(file).replace(/\.\w+$/, '')),
73+
)
74+
if (!iconId) {
75+
return undefined
76+
}
77+
78+
const icons = Array.isArray(iconId) ? iconId : [iconId]
79+
const modules: import('vite').ModuleNode[] = []
80+
for (const id of icons) {
81+
const iconPath = isIconPath(id)
82+
let module = iconPath ? server.moduleGraph.getModuleById(id) : undefined
83+
if (module) {
84+
modules.push(module)
85+
continue
86+
}
87+
if (!iconPath) {
88+
for (const prefix of ['~icons', 'virtual:icons', 'virtual/icons']) {
89+
module = server.moduleGraph.getModuleById(`${prefix}/${id}`)
90+
if (module) {
91+
modules.push(module)
92+
break
93+
}
94+
}
95+
}
96+
}
97+
return modules.length > 0 ? modules : undefined
98+
},
99+
},
61100
rollup: {
62101
api: {
63102
config: options,

src/types.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,54 @@ export interface Options {
106106
* @deprecated no longer needed
107107
*/
108108
iconSource?: 'legacy' | 'modern' | 'auto'
109+
110+
/**
111+
* HMR helper to resolve the icon id from local file SVG changes.
112+
*
113+
* **NOTE:** works only with Vite.
114+
*
115+
* Since there is no way to correlate the icon name with the local file SVG, we need a helper to invalidate the
116+
* corresponding icon module.
117+
*
118+
* For example, we can have a custom collection using `readFile`, we cannot resolve `~icons/inline/async`
119+
* when `<project-root>/assets/giftbox.svg` changes:
120+
* ```ts
121+
* customCollections: {
122+
* inline: {
123+
* async: () => fs.readFile('assets/giftbox.svg', 'utf-8')
124+
* }
125+
* }
126+
* ```
127+
*
128+
* To resolve the icon in the previous example you will need to add:
129+
* ```ts
130+
* hmrResolver(file) => file.endsWidth('assets/giftbox.svg') ? 'inline/async' : undefined
131+
* ```
132+
*
133+
* The `normalizedSVGIconName` is the SVG basename without the extension converted to kebab-case, will help you when using `FileSystemIconLoader`:
134+
* ```ts
135+
* customCollections: {
136+
* custom: FileSystemIconLoader('assets/custom-a')
137+
* }
138+
* ```
139+
*
140+
* then, to resolve the icons from the `assets/custom-a` folder you only need to add the corresponding collection name:
141+
* To resolve the icon in the previous example you will need to add:
142+
* ```ts
143+
* hmrResolver(file, folderName, normalizedSVGIconName) {
144+
* if (folderName.endsWith('assets/custom-a') {
145+
* return `custom/${normalizedSVGIconName}`
146+
* }
147+
* }
148+
* ```
149+
*
150+
* @param file The file path received from the Vite's [handleHotUpdate](https://vite.dev/guide/api-plugin.html#handlehotupdate) hook.
151+
* @param folderName The normalized folder containing the file.
152+
* @param normalizedSVGIconName The normalized SVG name (basename without extension and the path).
153+
* @return The icon collection and name to invalidate (<collection>/<icon>).
154+
* @see https://vitejs.dev/guide/api-plugin.html#handlehotupdate
155+
*/
156+
hmrResolver?: (file: string, folderName: string, normalizedSVGIconName: string) => Awaitable<string | string[] | undefined>
109157
}
110158

111-
export type ResolvedOptions = Omit<Required<Options>, 'iconSource' | 'transform'> & Pick<Options, 'transform'>
159+
export type ResolvedOptions = Omit<Required<Options>, 'iconSource' | 'transform' | 'hmrResolver'> & Pick<Options, 'transform' | 'hmrResolver'>

0 commit comments

Comments
 (0)