From fbe364633d322880a50c9958ac610bef60a5681c Mon Sep 17 00:00:00 2001 From: Divyansh Singh <40380293+brc-dd@users.noreply.github.com> Date: Sat, 29 Aug 2026 00:51:46 +0530 Subject: [PATCH] refactor: qualified icon names everywhere, socialLinks-only bare mapping - parseIconName requires `collection:name`; bare simple-icons names are qualified by VPSocialLink alone, with a build warning suggesting qualification when a bare name reaches generation any other way - collection resolution keeps loadCollectionFromFS (any level of the project tree) and falls back generically to the @iconify-json/* packages in vitepress's own dependencies instead of hardcoding simple-icons; the attribution comment special case is dropped - fix the mpa/spa parity test on windows (path.basename, not split('/')) Co-Authored-By: Claude Fable 5 --- __tests__/base/emit.test.ts | 4 +- __tests__/e2e/icons/index.md | 2 +- __tests__/unit/node/icons.test.ts | 40 ++++++++-------- docs/en/reference/default-theme-config.md | 4 +- docs/en/reference/site-config.md | 6 +-- src/client/app/composables/icon.ts | 9 ++-- .../theme-default/components/VPIcon.vue | 6 +-- .../theme-default/components/VPSocialLink.vue | 11 ++++- src/node/icons.ts | 48 +++++++++++-------- src/node/siteConfig.ts | 6 +-- src/shared/shared.ts | 20 ++++---- types/shared.d.ts | 4 +- 12 files changed, 90 insertions(+), 70 deletions(-) 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 +)