diff --git a/__tests__/base/emit.test.ts b/__tests__/base/emit.test.ts
index de275f7a..aa805e6a 100644
--- a/__tests__/base/emit.test.ts
+++ b/__tests__/base/emit.test.ts
@@ -1,5 +1,5 @@
import { readFileSync, readdirSync } from 'node:fs'
-import { join, resolve } from 'node:path'
+import { basename, join, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const dir = resolve(fileURLToPath(import.meta.url), '..')
@@ -191,7 +191,7 @@ describe('mpa + relative base emit', () => {
const mpa = find('mpa')
const plain = find('plain')
// same icon set — same content, same hash, mode-independent
- expect(mpa.split('/').pop()).toBe(plain.split('/').pop())
+ expect(basename(mpa)).toBe(basename(plain))
expect(readFileSync(mpa, 'utf-8')).toBe(readFileSync(plain, 'utf-8'))
})
})
diff --git a/__tests__/e2e/icons/index.md b/__tests__/e2e/icons/index.md
index 7f5fdb02..9bce3c59 100644
--- a/__tests__/e2e/icons/index.md
+++ b/__tests__/e2e/icons/index.md
@@ -5,7 +5,7 @@ import { VPIcon } from 'vitepress/theme'
-
+
Prose about the build internals must survive the rewrite pass:
diff --git a/__tests__/unit/node/icons.test.ts b/__tests__/unit/node/icons.test.ts
index a1f28081..35e3303e 100644
--- a/__tests__/unit/node/icons.test.ts
+++ b/__tests__/unit/node/icons.test.ts
@@ -10,22 +10,20 @@ const e2eRoot = resolve(fileURLToPath(import.meta.url), '../../../e2e')
describe('node/icons', () => {
describe('parseIconName', () => {
- test('bare names resolve in simple-icons', () => {
- expect(parseIconName('github')).toEqual({
- collection: 'simple-icons',
- icon: 'github'
- })
- })
-
- test('prefixed names resolve in their collection', () => {
+ test('parses qualified names', () => {
expect(parseIconName('lucide:heart')).toEqual({
collection: 'lucide',
icon: 'heart'
})
+ expect(parseIconName('simple-icons:github')).toEqual({
+ collection: 'simple-icons',
+ icon: 'github'
+ })
})
- test('rejects names outside iconify grammar', () => {
+ test('rejects bare names and anything outside iconify grammar', () => {
for (const name of [
+ 'github',
'GitHub',
'foo bar',
'foo:',
@@ -42,40 +40,44 @@ describe('node/icons', () => {
describe('generateIconsCSS', () => {
test('emits base rules and per-icon rules, no legacy common rule', async () => {
+ // simple-icons is not in the e2e workspace's package.json — this also
+ // covers the fallback to vitepress's own dependency
const { css, warnings } = await generateIconsCSS(
e2eRoot,
- new Set(['github']),
+ new Set(['simple-icons:github']),
'compressed'
)
expect(warnings).toEqual([])
expect(css).toContain(
'.vpi-simple-icons-github{--icon:url("data:image/svg+xml'
)
- expect(css).toContain('simple-icons (CC0 1.0)')
expect(css).toContain(":where([class^='vpi-']")
expect(css).toContain('display:inline-block')
expect(css).not.toContain('.vpi-social')
})
- test('credits simple-icons only when it contributed rules', async () => {
- const { css } = await generateIconsCSS(
+ test('suggests qualification for bare names', async () => {
+ const { css, warnings } = await generateIconsCSS(
e2eRoot,
- new Set(['notarealiconname', 'lucide:heart']),
+ new Set(['github']),
'compressed'
)
- expect(css).toContain('.vpi-lucide-heart')
- expect(css).not.toContain('simple-icons (CC0 1.0)')
+ expect(css).toBe('')
+ expect(warnings).toEqual([
+ expect.stringContaining('"github" has no collection prefix')
+ ])
+ expect(warnings[0]).toContain('simple-icons:github')
})
test('groups collections and stays deterministic across insertion order', async () => {
const a = await generateIconsCSS(
e2eRoot,
- new Set(['lucide:heart', 'github', 'lucide:egg']),
+ new Set(['lucide:heart', 'simple-icons:github', 'lucide:egg']),
'compressed'
)
const b = await generateIconsCSS(
e2eRoot,
- new Set(['github', 'lucide:egg', 'lucide:heart']),
+ new Set(['simple-icons:github', 'lucide:egg', 'lucide:heart']),
'compressed'
)
expect(a.css).toBe(b.css)
@@ -87,7 +89,7 @@ describe('node/icons', () => {
test('warns on icons missing from an installed collection', async () => {
const { css, warnings } = await generateIconsCSS(
e2eRoot,
- new Set(['github', 'thisiconisnotreal']),
+ new Set(['simple-icons:github', 'simple-icons:thisiconisnotreal']),
'compressed'
)
expect(css).toContain('.vpi-simple-icons-github')
diff --git a/docs/en/reference/default-theme-config.md b/docs/en/reference/default-theme-config.md
index 6737d86c..213fa347 100644
--- a/docs/en/reference/default-theme-config.md
+++ b/docs/en/reference/default-theme-config.md
@@ -280,9 +280,9 @@ interface SocialLink {
}
```
-Icon styles are generated at build time from collections installed locally, and dev mode serves them from the dev server — no icon is ever fetched from an external service. Icons rendered only on the client (e.g. inside ``) can't be detected during the build; list them in [`icons.include`](site-config#icons) instead.
+Icon styles are generated at build time from collections declared in your project's dependencies, and dev mode serves them from the dev server — no icon is ever fetched from an external service. Bare names are a `socialLinks` convenience and map to simple-icons; everywhere else icons are written as `collection:name`. Icons rendered only on the client (e.g. inside ``) can't be detected during the build; list them in [`icons.include`](site-config#icons) instead.
-To render one of these icons in your own Markdown or components, use the `VPIcon` component from `vitepress/theme` (``), or the lower-level `useIcon` composable from `vitepress` when building a custom theme.
+To render one of these icons in your own Markdown or components, use the `VPIcon` component from `vitepress/theme` (``), or the lower-level `useIcon` composable from `vitepress` when building a custom theme — both take fully qualified names.
## footer
diff --git a/docs/en/reference/site-config.md b/docs/en/reference/site-config.md
index c0fc5e05..a04516bd 100644
--- a/docs/en/reference/site-config.md
+++ b/docs/en/reference/site-config.md
@@ -493,14 +493,14 @@ Only production builds are affected. `vitepress preview` serves a root-absolute
- Type: `{ include?: string[] }`
-Options for the generated icon styles. The build collects every iconify icon rendered during SSR ([social links](default-theme-config#sociallinks), the `VPIcon` theme component, or any element registered through the `useIcon` composable) and emits their styles as a hashed `assets/vp-icons..css` asset. Names are `name` (resolved in [simple-icons](https://simpleicons.org/)) or `collection:name` for any `@iconify-json/*` collection installed in your project.
+Options for the generated icon styles. The build collects every iconify icon rendered during SSR ([social links](default-theme-config#sociallinks), the `VPIcon` theme component, or any element registered through the `useIcon` composable) and emits their styles as a hashed `assets/vp-icons..css` asset. Names are fully qualified as `collection:name`, resolved against the `@iconify-json/*` packages declared in your project's dependencies (`socialLinks` is the one place bare names are accepted — they map to [simple-icons](https://simpleicons.org/), which VitePress itself depends on).
Icons rendered only on the client — inside ``, or after hydration — are invisible to SSR collection. List them in `include` to force them into the stylesheet:
```ts
export default {
icons: {
- include: ['mdi:home', 'discord']
+ include: ['mdi:home', 'simple-icons:discord']
}
}
```
@@ -680,7 +680,7 @@ interface SSGContext {
}
```
-Custom themes can add icon names (`name` for simple-icons, or `collection:name`) to `vpIcons` during SSR to have their styles emitted — the `useIcon` composable from `vitepress` does this for you.
+Custom themes can add qualified `collection:name` icon names to `vpIcons` during SSR to have their styles emitted — the `useIcon` composable from `vitepress` does this for you.
### transformHead
diff --git a/src/client/app/composables/icon.ts b/src/client/app/composables/icon.ts
index d17fcd89..23b3b86a 100644
--- a/src/client/app/composables/icon.ts
+++ b/src/client/app/composables/icon.ts
@@ -16,10 +16,11 @@ import { withBase } from '../utils'
* name is registered so the build emits its CSS rule; in dev the icon is
* resolved from locally installed collections, without network access.
*
- * Accepts `name` (resolved in simple-icons) or `collection:name` for any
- * `@iconify-json/*` collection installed in the project. Returns the class
- * to render (`vpi--`); pass the template ref of the
- * element carrying it so dev can apply the on-demand fallback.
+ * Accepts a fully qualified `collection:name` for any `@iconify-json/*`
+ * collection in the project's dependencies (e.g. `simple-icons:github`).
+ * Returns the class to render (`vpi--`); pass the
+ * template ref of the element carrying it so dev can apply the on-demand
+ * fallback.
*/
export function useIcon(
icon: MaybeRefOrGetter,
diff --git a/src/client/theme-default/components/VPIcon.vue b/src/client/theme-default/components/VPIcon.vue
index 55935044..57cd1dcd 100644
--- a/src/client/theme-default/components/VPIcon.vue
+++ b/src/client/theme-default/components/VPIcon.vue
@@ -4,9 +4,9 @@ import { useTemplateRef } from 'vue'
const props = defineProps<{
/**
- * `name` (a simple-icons name) or `collection:name` for any
- * `@iconify-json/*` collection installed in the project, or a raw
- * `{ svg }` string.
+ * A fully qualified `collection:name` for any `@iconify-json/*`
+ * collection in the project's dependencies (e.g. `simple-icons:github`,
+ * `lucide:rocket`), or a raw `{ svg }` string.
*/
icon: string | { svg: string }
}>()
diff --git a/src/client/theme-default/components/VPSocialLink.vue b/src/client/theme-default/components/VPSocialLink.vue
index 2009dad9..d77a2a1a 100644
--- a/src/client/theme-default/components/VPSocialLink.vue
+++ b/src/client/theme-default/components/VPSocialLink.vue
@@ -14,7 +14,16 @@ const props = defineProps<{
}>()
const el = useTemplateRef('el')
-const iconClass = useIcon(() => props.icon, el)
+
+// socialLinks accepts bare simple-icons names; everything downstream
+// speaks fully qualified `collection:name`
+const iconClass = useIcon(
+ () =>
+ typeof props.icon === 'string' && !props.icon.includes(':')
+ ? `simple-icons:${props.icon}`
+ : props.icon,
+ el
+)
diff --git a/src/node/icons.ts b/src/node/icons.ts
index 1260ba38..8939aaad 100644
--- a/src/node/icons.ts
+++ b/src/node/icons.ts
@@ -5,12 +5,21 @@ import { formatCSS } from '@iconify/utils/lib/css/format'
import { getIconsCSSData } from '@iconify/utils/lib/css/icons'
import { loadCollectionFromFS } from '@iconify/utils/lib/loader/fs'
-import { DEFAULT_ICONS_COLLECTION, parseIconName } from './shared'
+import { dependencies } from '../../package.json' with { type: 'json' }
+import { parseIconName } from './shared'
type IconifyJSON = Parameters[0]
const require = createRequire(import.meta.url)
+// collections vitepress itself depends on (simple-icons today) — resolvable
+// through vitepress even when the project doesn't install them
+const ownCollections = new Set(
+ Object.keys(dependencies)
+ .filter((dep) => dep.startsWith('@iconify-json/'))
+ .map((dep) => dep.slice('@iconify-json/'.length))
+)
+
/**
* Replaced with the content hash (or stripped together with the link tag when
* no icons are used) after all pages have rendered — the icon set, and hence
@@ -48,16 +57,20 @@ async function loadCollection(
name: string,
root: string
): Promise {
- if (name === DEFAULT_ICONS_COLLECTION) {
- // vitepress's own dependency — the user's root may not resolve it
- return require('@iconify-json/simple-icons/icons.json')
- }
const key = `${root}\0${name}`
let cached = collectionCache.get(key)
if (!cached) {
- cached = loadCollectionFromFS(name, false, '@iconify-json', root).catch(
- () => undefined
- )
+ // resolvable from anywhere in the project's tree; falls back to
+ // vitepress's own dependencies for the collections it ships
+ cached = loadCollectionFromFS(name, false, '@iconify-json', root)
+ .catch(() => undefined)
+ .then(
+ (data) =>
+ data ??
+ (ownCollections.has(name)
+ ? require(`@iconify-json/${name}/icons.json`)
+ : undefined)
+ )
collectionCache.set(key, cached)
// don't memoize a miss — the user may install the collection while the
// dev server is running
@@ -79,7 +92,13 @@ export async function generateIconsCSS(
for (const raw of icons) {
const parsed = parseIconName(raw)
if (!parsed) {
- warnings.push(`"${raw}" is not a valid icon name and was skipped.`)
+ warnings.push(
+ !raw.includes(':') && parseIconName(`x:${raw}`)
+ ? `"${raw}" has no collection prefix — write it as ` +
+ `":${raw}" (e.g. "simple-icons:${raw}"). Only ` +
+ `\`socialLinks\` qualifies bare names automatically.`
+ : `"${raw}" is not a valid icon name and was skipped.`
+ )
continue
}
let names = byCollection.get(parsed.collection)
@@ -88,7 +107,6 @@ export async function generateIconsCSS(
}
const chunks: string[] = []
- let hasSimpleIcons = false
for (const collection of Array.from(byCollection.keys()).sort()) {
const data = await loadCollection(collection, root)
@@ -115,19 +133,11 @@ export async function generateIconsCSS(
mode: 'mask'
})
chunks.push(formatCSS(cssData.css, format))
- if (collection === DEFAULT_ICONS_COLLECTION) hasSimpleIcons = true
}
if (!chunks.length) return { css: '', warnings }
- const attribution = hasSimpleIcons
- ? '/* simple-icons (CC0 1.0) — https://simpleicons.org */\n'
- : ''
-
- return {
- css: attribution + BASE_RULES + '\n' + chunks.join(''),
- warnings
- }
+ return { css: BASE_RULES + '\n' + chunks.join(''), warnings }
}
const collectionMissingMessage = (collection: string) =>
diff --git a/src/node/siteConfig.ts b/src/node/siteConfig.ts
index 342f7a4d..03bebf90 100644
--- a/src/node/siteConfig.ts
+++ b/src/node/siteConfig.ts
@@ -140,9 +140,9 @@ export interface UserConfig<
* Icons to include in the generated stylesheet in addition to the ones
* collected while rendering pages — needed for icons that only render
* client-side (e.g. inside ``), which SSR collection cannot
- * see. Names are `name` (simple-icons) or `collection:name` for any
- * installed `@iconify-json/*` collection.
- * @example ['mdi:home', 'discord']
+ * see. Names are fully qualified as `collection:name`, for any
+ * `@iconify-json/*` collection in the project's dependencies.
+ * @example ['mdi:home', 'simple-icons:discord']
*/
include?: string[]
}
diff --git a/src/shared/shared.ts b/src/shared/shared.ts
index c4feb4f9..2436e3ea 100644
--- a/src/shared/shared.ts
+++ b/src/shared/shared.ts
@@ -30,26 +30,24 @@ export type {
export const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i
export const APPEARANCE_KEY = 'vitepress-theme-appearance'
-/** collection bare icon names resolve in — the one vitepress ships itself */
-export const DEFAULT_ICONS_COLLECTION = 'simple-icons'
-
// iconify's icon/collection name grammar
const iconNameRE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
/**
- * Parse `name` (resolved in [[DEFAULT_ICONS_COLLECTION]]) or
- * `collection:name` for any installed `@iconify-json/*` collection.
- * The corresponding class is `vpi--`. Returns null for
- * names that don't fit iconify's grammar (also keeping malformed input
- * out of generated selectors and class attributes).
+ * Parse a fully qualified `collection:name` icon name (any installed
+ * `@iconify-json/*` collection). The corresponding class is
+ * `vpi--`. Returns null for anything else — bare names
+ * included — which also keeps malformed input out of generated selectors
+ * and class attributes. (`socialLinks` additionally accepts bare
+ * simple-icons names; the theme qualifies them before they get here.)
*/
export function parseIconName(
name: string
): { collection: string; icon: string } | null {
const colon = name.indexOf(':')
- const collection =
- colon === -1 ? DEFAULT_ICONS_COLLECTION : name.slice(0, colon)
- const icon = colon === -1 ? name : name.slice(colon + 1)
+ if (colon === -1) return null
+ const collection = name.slice(0, colon)
+ const icon = name.slice(colon + 1)
if (!iconNameRE.test(collection) || !iconNameRE.test(icon)) return null
return { collection, icon }
}
diff --git a/types/shared.d.ts b/types/shared.d.ts
index 2c3e1c93..6aa88743 100644
--- a/types/shared.d.ts
+++ b/types/shared.d.ts
@@ -366,8 +366,8 @@ export interface SSGContext extends SSRContext {
/**
* The icons used on the page, collected during SSR so that only the
* styles of used icons are emitted into the generated stylesheet.
- * Names are `name` (resolved in simple-icons) or `collection:name` for
- * any `@iconify-json/*` collection installed in the project. Theme
+ * Names are fully qualified as `collection:name`, for any
+ * `@iconify-json/*` collection in the project's dependencies. Theme
* components register icons here (see the `useIcon` composable).
*/
vpIcons: Set