From 3aec879d0096dc41d9e67705c86d1c32fab118e0 Mon Sep 17 00:00:00 2001 From: userquin Date: Sun, 2 Nov 2025 16:45:24 +0100 Subject: [PATCH] feat(vite): add HMR support for local SVG --- examples/vite-vue3/vite.config.ts | 9 ++++++ src/core/options.ts | 2 ++ src/index.ts | 39 ++++++++++++++++++++++++ src/types.ts | 50 ++++++++++++++++++++++++++++++- 4 files changed, 99 insertions(+), 1 deletion(-) diff --git a/examples/vite-vue3/vite.config.ts b/examples/vite-vue3/vite.config.ts index f109ded..4255cff 100644 --- a/examples/vite-vue3/vite.config.ts +++ b/examples/vite-vue3/vite.config.ts @@ -45,6 +45,15 @@ const config: UserConfig = { props.color = 'skyblue' } }, + hmrResolver(file, folder, normalizedSVGIconName) { + console.log(file, folder, normalizedSVGIconName) + if (file.endsWith('assets/giftbox.svg')) { + return 'inline/async' + } + if (folder.endsWith('assets/custom-a')) { + return `custom/${normalizedSVGIconName}` + } + }, }), Components({ dts: true, diff --git a/src/core/options.ts b/src/core/options.ts index d769595..a0cc570 100644 --- a/src/core/options.ts +++ b/src/core/options.ts @@ -17,6 +17,7 @@ export async function resolveOptions(options: Options): Promise transform, autoInstall = false, collectionsNodeResolvePath = process.cwd(), + hmrResolver, } = options const webComponents = Object.assign({ @@ -38,6 +39,7 @@ export async function resolveOptions(options: Options): Promise transform, autoInstall, collectionsNodeResolvePath, + hmrResolver, } } diff --git a/src/index.ts b/src/index.ts index b930d9a..04dbb7c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import type { Options } from './types' +import { basename, dirname } from 'node:path' +import { camelToKebab } from '@iconify/utils/lib/misc/strings' import { createUnplugin } from 'unplugin' import { generateComponentFromPath, isIconPath, normalizeIconPath, resolveIconsPath } from './core/loader' import { resolveOptions } from './core/options' @@ -58,6 +60,43 @@ const unplugin = createUnplugin((options = {}) => { } } }, + vite: { + async handleHotUpdate({ file, server }) { + const hmrResolver = await resolved.then(({ hmrResolver }) => hmrResolver) + if (!hmrResolver) { + return undefined + } + const iconId = await hmrResolver( + file, + dirname(file).replace(/\\/g, '/'), + camelToKebab(basename(file).replace(/\.\w+$/, '')), + ) + if (!iconId) { + return undefined + } + + const icons = Array.isArray(iconId) ? iconId : [iconId] + const modules: import('vite').ModuleNode[] = [] + for (const id of icons) { + const iconPath = isIconPath(id) + let module = iconPath ? server.moduleGraph.getModuleById(id) : undefined + if (module) { + modules.push(module) + continue + } + if (!iconPath) { + for (const prefix of ['~icons', 'virtual:icons', 'virtual/icons']) { + module = server.moduleGraph.getModuleById(`${prefix}/${id}`) + if (module) { + modules.push(module) + break + } + } + } + } + return modules.length > 0 ? modules : undefined + }, + }, rollup: { api: { config: options, diff --git a/src/types.ts b/src/types.ts index a90ec79..2311049 100644 --- a/src/types.ts +++ b/src/types.ts @@ -106,6 +106,54 @@ export interface Options { * @deprecated no longer needed */ iconSource?: 'legacy' | 'modern' | 'auto' + + /** + * HMR helper to resolve the icon id from local file SVG changes. + * + * **NOTE:** works only with Vite. + * + * Since there is no way to correlate the icon name with the local file SVG, we need a helper to invalidate the + * corresponding icon module. + * + * For example, we can have a custom collection using `readFile`, we cannot resolve `~icons/inline/async` + * when `/assets/giftbox.svg` changes: + * ```ts + * customCollections: { + * inline: { + * async: () => fs.readFile('assets/giftbox.svg', 'utf-8') + * } + * } + * ``` + * + * To resolve the icon in the previous example you will need to add: + * ```ts + * hmrResolver(file) => file.endsWidth('assets/giftbox.svg') ? 'inline/async' : undefined + * ``` + * + * The `normalizedSVGIconName` is the SVG basename without the extension converted to kebab-case, will help you when using `FileSystemIconLoader`: + * ```ts + * customCollections: { + * custom: FileSystemIconLoader('assets/custom-a') + * } + * ``` + * + * then, to resolve the icons from the `assets/custom-a` folder you only need to add the corresponding collection name: + * To resolve the icon in the previous example you will need to add: + * ```ts + * hmrResolver(file, folderName, normalizedSVGIconName) { + * if (folderName.endsWith('assets/custom-a') { + * return `custom/${normalizedSVGIconName}` + * } + * } + * ``` + * + * @param file The file path received from the Vite's [handleHotUpdate](https://vite.dev/guide/api-plugin.html#handlehotupdate) hook. + * @param folderName The normalized folder containing the file. + * @param normalizedSVGIconName The normalized SVG name (basename without extension and the path). + * @return The icon collection and name to invalidate (/). + * @see https://vitejs.dev/guide/api-plugin.html#handlehotupdate + */ + hmrResolver?: (file: string, folderName: string, normalizedSVGIconName: string) => Awaitable } -export type ResolvedOptions = Omit, 'iconSource' | 'transform'> & Pick +export type ResolvedOptions = Omit, 'iconSource' | 'transform' | 'hmrResolver'> & Pick