import fs from 'node:fs/promises'; import { addVitePlugin, getLayerDirectories, addTemplate, logger, defineNuxtModule, createResolver, getNuxtVersion, addPlugin, addComponent, addServerHandler, hasNuxtModule, resolvePath as resolvePath$1, updateTemplates } from '@nuxt/kit'; import { addCustomTab } from '@nuxt/devtools-kit'; import { resolvePath } from 'mlly'; import { relative } from 'node:path'; import { resolveModule } from 'local-pkg'; import { b as getCollectionPath, f as resolveCollectionFile, g as generateClientBundleCode, c as collectionNames, d as discoverInstalledCollections, e as resolveCollection, l as loadCustomCollection, I as IconUsageScanner, r as resolveBundleIcons } from './shared/icon.VmJ4USKj.mjs'; import { provider } from 'std-env'; import 'consola'; import 'tinyglobby'; import '@iconify/utils/lib/svg/parse'; import 'picomatch'; const schema = { $schema: { title: "Nuxt Icon", description: "Configure Nuxt Icon module preferences.", tags: ["@studioIcon material-symbols:star"] }, size: { $default: void 0, $schema: { title: "Icon Size", description: "Set the default icon size.", tags: ["@studioIcon material-symbols:format-size-rounded"], tsType: "string | undefined" } }, class: { $default: "", $schema: { title: "CSS Class", description: "Set the default CSS class.", tags: ["@studioIcon material-symbols:css"] } }, attrs: { $default: { "aria-hidden": true }, $schema: { title: "Default Attributes", description: [ "Attributes applied to every icon component.", "", '@default { "aria-hidden": true }' ].join("\n"), tags: ["@studioIcon material-symbols:settings"], tsType: "Record" } }, mode: { $default: "css", $schema: { title: "Default Rendering Mode", description: "Set the default rendering mode for the icon component", enum: ["css", "svg"], tags: ["@studioIcon material-symbols:move-down-rounded"] } }, aliases: { $default: {}, $schema: { title: "Icon aliases", description: "Define Icon aliases to update them easily without code changes.", tags: ["@studioIcon material-symbols:star-rounded"], tsType: "{ [alias: string]: string }" } }, cssSelectorPrefix: { $default: "i-", $schema: { title: "CSS Selector Prefix", description: "Set the default CSS selector prefix.", tags: ["@studioIcon material-symbols:format-textdirection-l-to-r"] } }, cssLayer: { $default: void 0, $schema: { title: "CSS Layer Name", description: "Set the default CSS `@layer` name.", tags: ["@studioIcon material-symbols:layers"], tsType: "string | undefined" } }, cssWherePseudo: { $default: true, $schema: { title: "Use CSS `:where()` Pseudo Selector", description: "Use CSS `:where()` pseudo selector to reduce specificity.", tags: ["@studioIcon material-symbols:low-priority"] } }, collections: { $default: null, $schema: { title: "Icon Collections", description: [ "List of known icon collections name. Used to resolve collection name ambiguity.", "e.g. `simple-icons-github` -> `simple-icons:github` instead of `simple:icons-github`", "", "When not provided, will use the full Iconify collection list." ].join("\n"), tags: ["@studioIcon material-symbols:format-list-bulleted"], tsType: "string[] | null" } }, customCollections: { $default: null, $schema: { title: "Custom Icon Collections", tags: ["@studioIcon material-symbols:format-list-bulleted"], tsType: "string[] | null" } }, provider: { $default: void 0, $schema: { title: "Icon Provider", description: [ "Provider to use for fetching icons", "", "- `server` - Fetch icons with a server handler", "- `iconify` - Fetch icons with Iconify API, purely client-side", "- `none` - Do not fetch icons (use client bundle only)", "", "`server` by default; `iconify` when `ssr: false`" ].join("\n"), enum: ["server", "iconify", "none"], tags: ["@studioIcon material-symbols:cloud"], type: '"server" | "iconify" | "none" | undefined' } }, iconifyApiEndpoint: { $default: "https://api.iconify.design", $schema: { title: "Iconify API Endpoint URL", description: "Define a custom Iconify API endpoint URL. Useful if you want to use a self-hosted Iconify API. Learn more: https://iconify.design/docs/api.", tags: ["@studioIcon material-symbols:api"] } }, fallbackToApi: { $default: true, $schema: { title: "Fallback to Iconify API", description: "Fallback to Iconify API if server provider fails to found the collection.", tags: ["@studioIcon material-symbols:public"], enum: [true, false, "server-only", "client-only"], type: 'boolean | "server-only" | "client-only"' } }, localApiEndpoint: { $default: "/api/_nuxt_icon", $schema: { title: "Local API Endpoint Path", description: "Define a custom path for the local API endpoint.", tags: ["@studioIcon material-symbols:api"] } }, fetchTimeout: { $default: 1500, $schema: { title: "Fetch Timeout", description: "Set the timeout for fetching icons.", tags: ["@studioIcon material-symbols:timer"] } }, customize: { $default: void 0, $schema: { title: "Customize callback", description: "Customize icon content (replace stroke-width, colors, etc...).", tags: ["@studioIcon material-symbols:edit"], type: "IconifyIconCustomizeCallback" } } }; function unocssIntegration(nuxt, options) { let uno; function getKnownIconClasses() { const cache = uno?._cache; if (cache) return Array.from(cache.entries()).filter(([key, value]) => value && key.startsWith(options.cssSelectorPrefix || "i-")).map(([key]) => key); return []; } nuxt.hook("vite:configResolved", (config, { isClient }) => { if (!isClient) return; uno = config.plugins?.flat().find((p) => p && "name" in p && p.name === "unocss:api")?.api?.getContext?.()?.uno; }); if (!nuxt.options.dev) { addVitePlugin( { name: "nuxt-icon:client-build-end", generateBundle() { if (uno) { options.serverKnownCssClasses ||= []; options.serverKnownCssClasses.push(...getKnownIconClasses()); } } }, { client: true, server: false } ); } if (nuxt.options.dev) { nuxt.hook("nitro:init", async (_nitro) => { _nitro.options.runtimeConfig.icon ||= {}; Object.defineProperty(_nitro.options.runtimeConfig.icon, "serverKnownCssClasses", { get() { return [ ...options.serverKnownCssClasses || [], ...getKnownIconClasses() ]; } }); }); } } function getResolvePaths(nuxt) { const layerDirs = getLayerDirectories(nuxt).map( (dir) => dir.root ); return Array.from( new Set( [nuxt.options.rootDir, nuxt.options.workspaceDir, ...layerDirs].filter( Boolean ) ) ); } function registerServerBundle(ctx) { const { nuxt } = ctx; const templateServer = addTemplate({ filename: "nuxt-icon-server-bundle.mjs", write: true, async getContents() { const { collections, remote } = await ctx.resolveServerBundle(); nuxt.options.appConfig.icon ||= {}; const appIcons = nuxt.options.appConfig.icon; appIcons.collections ||= []; for (const collection of collections) { const prefix = typeof collection === "string" ? collection : collection.prefix; if (!appIcons.collections.includes(prefix)) appIcons.collections.push(prefix); } const isBundling = !nuxt.options.dev; function getRemoteEndpoint(name) { if (typeof remote === "function") return remote(name); switch (remote) { case "jsdelivr": return `https://cdn.jsdelivr.net/npm/@iconify-json/${name}/icons.json`; case "unpkg": return `https://unpkg.com/@iconify-json/${name}/icons.json`; case "github-raw": return `https://raw.githubusercontent.com/iconify/icon-sets/master/json/${name}.json`; default: throw new Error(`Unknown remote collection source: ${remote}`); } } const collectionsValues = collections.map((collection) => { if (typeof collection === "string") { if (remote) { return ` '${collection}': createRemoteCollection(${JSON.stringify(getRemoteEndpoint(collection))}),`; } const resolvePaths = getResolvePaths(nuxt); const path = getCollectionPath(collection, resolvePaths); if (!isBundling) { return ` '${collection}': () => require('${path}'),`; } const file = resolveModule(path, { paths: [nuxt.options.rootDir] }) ? void 0 : resolveCollectionFile(collection, resolvePaths); if (file) { const relPath = relative(nuxt.options.buildDir, file).replaceAll("\\", "/"); return ` '${collection}': () => import('${relPath.startsWith(".") ? relPath : `./${relPath}`}').then(m => m.default),`; } return ` '${collection}': () => import('${path}', { with: { type: 'json' } }).then(m => m.default),`; } else { const { prefix } = collection; if ("fetchEndpoint" in collection) return ` '${prefix}': createRemoteCollection(${JSON.stringify(collection.fetchEndpoint)}),`; return ` '${prefix}': () => (${JSON.stringify(collection)}),`; } }); const lines = [ ...isBundling ? [] : [ `import { createRequire } from 'node:module'`, `const require = createRequire(import.meta.url)` ], `function createRemoteCollection(fetchEndpoint) {`, " let _cache", " return async () => {", " if (_cache)", " return _cache", " const res = await fetch(fetchEndpoint).then(r => r.json())", " _cache = res", " return res", " }", "}", "", `export const collections = {`, ...collectionsValues, "}" ]; return lines.join("\n"); } }); nuxt.options.nitro.alias ||= {}; nuxt.options.nitro.alias["#nuxt-icon-server-bundle"] = templateServer.dst; } function registerClientBundle(ctx) { let cacheSize = 0; let cacheVersion = -1; let cacheData = null; addTemplate({ filename: "nuxt-icon-client-bundle.mjs", write: true, async getContents() { const { sizeLimitKb = 256 } = ctx.options.clientBundle || {}; const { collections, count, failed, dropped } = await ctx.loadClientBundleCollections(); if (cacheSize === count && cacheData && cacheVersion === ctx.clientBundleVersion) { return cacheData; } if (failed.length) { const msg = `Nuxt Icon could not fetch the icon data for client bundle: ${failed.map((f) => " - " + f).join("\n")}`; if (!ctx.nuxt.options.dev) throw new Error(msg); else logger.warn(msg); } if (dropped.length) { logger.debug(`Nuxt Icon could not resolve these icons for the client bundle, falling back to runtime loading: ${dropped.map((f) => " - " + f).join("\n")}`); } const { code, bundleSizeKb } = generateClientBundleCode(collections, { sizeLimitKb }); if (collections.length) logger.info(`Nuxt Icon client bundle consist of \`${count}\` icons with \`${bundleSizeKb.toFixed(2)}KB\`(uncompressed) in size`); cacheData = code; cacheSize = count; cacheVersion = ctx.clientBundleVersion; return cacheData; } }); } const KEYWORDS_EDGE_TARGETS = [ "edge", "cloudflare", "worker" ]; class NuxtIconModuleContext { constructor(nuxt, options) { this.nuxt = nuxt; this.options = options; } clientBundleVersion = 0; scannedIcons = /* @__PURE__ */ new Set(); scanner; getRuntimeCollections(runtimeOptions) { const resolved = runtimeOptions.fallbackToApi ? collectionNames : typeof this.options.serverBundle === "string" ? collectionNames : this.options.serverBundle ? this.options.serverBundle.collections?.map((c) => typeof c === "string" ? c : c.prefix) || [] : []; for (const collection of this.options.customCollections || []) { if (collection.prefix && !resolved.includes(collection.prefix)) resolved.push(collection.prefix); } return resolved; } _customCollections; _serverBundle; _nitroPreset; setNitroPreset(preset) { this._nitroPreset = preset || this._nitroPreset; } async resolveServerBundle() { if (!this._serverBundle) { this._serverBundle = this._resolveServerBundle().then((bundle) => { if (this._serverBundle) this._serverBundle = bundle; return bundle; }); } return this._serverBundle; } async _resolveServerBundle() { let serverBundle = this.options.serverBundle; if (serverBundle === "auto") { const preset = this._nitroPreset || (typeof this.nuxt.options.nitro.preset === "string" ? this.nuxt.options.nitro.preset || provider : provider); serverBundle = "local"; if (!this.nuxt.options.dev && KEYWORDS_EDGE_TARGETS.some( (word) => typeof preset === "string" && preset.includes(word) || process.env.NITRO_PRESET?.includes(word) || process.env.SERVER_PRESET?.includes(word) )) serverBundle = "remote"; logger.info(`Nuxt Icon server bundle mode is set to \`${serverBundle}\``); } const resolved = !serverBundle || this.options.provider !== "server" ? { disabled: true } : typeof serverBundle === "string" ? { remote: serverBundle === "remote" } : serverBundle; if (resolved.disabled) { return { disabled: true, remote: false, externalizeIconsJson: false, collections: [] }; } if (!resolved.collections) resolved.collections = resolved.remote ? collectionNames : await discoverInstalledCollections(getResolvePaths(this.nuxt)); const collections = await Promise.all( (resolved.collections || []).map((c) => resolveCollection(c, this.nuxt.options.rootDir)) ); return { disabled: false, remote: resolved.remote === true ? "jsdelivr" : resolved.remote || false, externalizeIconsJson: !!resolved.externalizeIconsJson, collections: [ ...collections, ...await this.loadCustomCollection() ] }; } async loadCustomCollection(force = false) { if (force) { this.clientBundleVersion += 1; this._customCollections = void 0; } if (!this._customCollections) { this._customCollections = this._loadCustomCollection().then((collections) => { if (this._customCollections) this._customCollections = collections; return collections; }); } return this._customCollections; } async _loadCustomCollection() { return Promise.all( (this.options.customCollections || []).map((collection) => loadCustomCollection(collection, this.nuxt.options.rootDir)) ); } async loadClientBundleCollections() { const { includeCustomCollections = this.options.provider !== "server", scan = false } = this.options.clientBundle || {}; const userIcons = new Set((this.options.clientBundle?.icons || []).map((i) => i.replace(/^i[-:]/, ""))); let customCollections = []; if (this.options.customCollections?.length) { customCollections = await this.loadCustomCollection(); } if (scan && !this.scanner) { const additionalCollections = customCollections.map((c) => c.prefix); const scanOptions = scan === true ? { additionalCollections } : { ...scan, additionalCollections: [ ...additionalCollections, ...scan.additionalCollections || [] ] }; this.scanner = new IconUsageScanner(scanOptions); await this.scanner.scanFiles( this.nuxt.options._layers.map((layer) => layer.cwd), this.scannedIcons ); } const icons = /* @__PURE__ */ new Set([...userIcons, ...this.scannedIcons]); await this.nuxt.callHook("icon:clientBundleIcons", icons); return resolveBundleIcons({ icons: [...userIcons].filter((i) => icons.has(i)), scannedIcons: [...this.scannedIcons].filter((i) => icons.has(i)), extraIcons: [...icons].filter((i) => !userIcons.has(i) && !this.scannedIcons.has(i)), customCollections, includeCustomCollections, // Resolve collections from the Nuxt root (and workspace root as a fallback) // instead of `process.cwd()`, so collections installed in a subproject are // found when the command is launched from a workspace root. resolvePaths: getResolvePaths(this.nuxt) }); } } const module$1 = defineNuxtModule({ meta: { name: "@nuxt/icon", configKey: "icon", compatibility: { nuxt: ">=4.0.0" } }, defaults: { // Module options componentName: "Icon", serverBundle: "auto", serverKnownCssClasses: [], clientBundle: { icons: [] }, // Runtime options provider: schema["provider"].$default, class: schema["class"].$default, size: schema["size"].$default, aliases: schema["aliases"].$default, iconifyApiEndpoint: schema["iconifyApiEndpoint"].$default, localApiEndpoint: schema["localApiEndpoint"].$default, fallbackToApi: schema["fallbackToApi"].$default, cssSelectorPrefix: schema["cssSelectorPrefix"].$default, cssWherePseudo: schema["cssWherePseudo"].$default, cssLayer: schema["cssLayer"].$default, mode: schema["mode"].$default, attrs: schema["attrs"].$default, collections: schema["collections"].$default, fetchTimeout: schema["fetchTimeout"].$default }, async setup(options, nuxt) { const resolver = createResolver(import.meta.url); const usesNitroV3 = Number.parseInt(getNuxtVersion(nuxt)) >= 5; nuxt.options.nitro.virtual ||= {}; nuxt.options.nitro.virtual["#nuxt-icon-server-runtime"] = usesNitroV3 ? `export { defineCachedHandler } from 'nitro/cache'` : `export { defineCachedEventHandler as defineCachedHandler } from 'nitropack/runtime'`; nuxt.options.nitro.virtual["#nuxt-icon-server-options"] = () => `export default ${JSON.stringify({ iconifyApiEndpoint: options.iconifyApiEndpoint, fallbackToApi: options.fallbackToApi })}`; if (typeof options.customize === "function") { throw new TypeError("`customize` callback cannot be set in module options, use `app.config.ts` or component props instead."); } if (!options.provider) { options.provider = !nuxt.options.ssr || nuxt.options.nitro.static || nuxt.options._generate ? "iconify" : "server"; } nuxt.options.vite ||= {}; nuxt.options.vite.resolve ||= {}; nuxt.options.vite.resolve.dedupe ||= []; nuxt.options.vite.resolve.dedupe.push("@iconify/vue"); const ctx = new NuxtIconModuleContext(nuxt, options); addPlugin( resolver.resolve("./runtime/plugin") ); addComponent({ name: options.componentName || "Icon", global: true, filePath: await resolver.resolvePath("./runtime/components/index") }); if (options.provider === "server") { addServerHandler({ route: `${options.localApiEndpoint || "/api/_nuxt_icon"}/:collection`, handler: resolver.resolve("./runtime/server/api") }); } await setupCustomCollectionsWatcher(options, nuxt, ctx); const runtimeOptions = Object.fromEntries( Object.entries(options).filter(([key]) => key in schema) ); if (!runtimeOptions.collections) { runtimeOptions.collections = ctx.getRuntimeCollections(runtimeOptions); } nuxt.options.appConfig.icon = Object.assign( nuxt.options.appConfig.icon || {}, runtimeOptions, { customCollections: options.customCollections?.map((i) => i.prefix) } ); nuxt.hook("schema:extend", (schemas) => { schemas.push({ appConfig: { icon: schema } }); }); nuxt.hook("nitro:config", async (nitroConfig) => { ctx.setNitroPreset(nitroConfig.preset); const bundle = await ctx.resolveServerBundle(); if (bundle.remote || !bundle.externalizeIconsJson) return; logger.warn("Nuxt Icon's `serverBundle.externalizeIconsJson` is an experimental feature, it requires that your production Node.js server is able to import JSON modules."); const collections = bundle.collections.filter((collection) => typeof collection === "string").map((collection) => getCollectionPath(collection, getResolvePaths(nuxt))); const resolvedPaths = await Promise.all( collections.map((collection) => resolvePath(collection, { url: nuxt.options.rootDir })) ); nitroConfig.externals ||= {}; nitroConfig.externals.traceInclude ||= []; nitroConfig.externals.traceInclude.push(...resolvedPaths); nitroConfig.rollupConfig ||= {}; nitroConfig.rollupConfig.plugins ||= []; nitroConfig.rollupConfig.plugins.unshift({ name: "@nuxt/icon:rollup", resolveId(id) { if (id.match(/(?:[\\/]|^)(@iconify-json[\\/]|@iconify[\\/]json)/)) { return { id, external: true }; } }, // Rollup 4 re-prints the import attributes of external modules with the `assert` // key (`output.importAttributesKey`, switching to `with` only in Rollup 5), and // Node.js removed `assert` in v22 — the icon API then fails at runtime with // `ERR_IMPORT_ATTRIBUTE_MISSING`. Keep the key the generated code already uses. outputOptions(options2) { options2.importAttributesKey ||= "with"; return options2; } }); }); registerServerBundle(ctx); registerClientBundle(ctx); addCustomTab({ name: "icones", title: "Ic\xF4nes", icon: "https://icones.js.org/favicon.svg", view: { type: "iframe", src: "https://icones.js.org" } }); options.serverKnownCssClasses ||= []; const serverKnownCssClasses = options.serverKnownCssClasses || []; nuxt.options.runtimeConfig.icon = { serverKnownCssClasses }; nuxt.hook("nitro:init", async (_nitro) => { _nitro.options.runtimeConfig.icon = { serverKnownCssClasses }; }); if (hasNuxtModule("@unocss/nuxt")) unocssIntegration(nuxt, options); await nuxt.callHook("icon:serverKnownCssClasses", serverKnownCssClasses); } }); async function setupCustomCollectionsWatcher(options, nuxt, ctx) { if (!options.customCollections?.length) return; let viteDevServer; const collectionDirs = await Promise.all(options.customCollections.filter((x) => "dir" in x).map((x) => resolvePath$1(x.dir))); if (options.clientBundle?.includeCustomCollections) { addVitePlugin({ name: "nuxt-icon/client-bundle-updater", apply: "serve", configureServer(server) { viteDevServer = server; } }); } nuxt.hook("builder:watch", async (event, path) => { const resolvedPath = await resolvePath$1(path); if (ctx.scanner) { ctx.scanner.extractFromCode( await fs.readFile(resolvedPath, "utf-8").catch(() => ""), ctx.scannedIcons ); } if (collectionDirs.some((cd) => resolvedPath.startsWith(cd))) { await ctx.loadCustomCollection(true); await updateTemplates({ filter: (template) => template.filename.startsWith("nuxt-icon-") }); if (viteDevServer) { const nuxtIconClientBundleModule = await viteDevServer.moduleGraph.getModuleByUrl("/.nuxt/nuxt-icon-client-bundle.mjs"); if (nuxtIconClientBundleModule) { viteDevServer.moduleGraph.invalidateModule(nuxtIconClientBundleModule); await viteDevServer.reloadModule(nuxtIconClientBundleModule); } } } }); } export { module$1 as default };