Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions src/runtime/components/resolve-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { computed, reactive, toRaw } from 'vue'
import type { NuxtIconRuntimeOptions } from '../../types'

const HYPHEN = 45

const sortedCollectionsCache = new WeakMap<string[], { readonly value: string[] }>()

export function getSortedCollections(collections: string[] | null | undefined): string[] {
if (!collections)
return []
const key = toRaw(collections)
let sorted = sortedCollectionsCache.get(key)
if (!sorted) {
const sort = (source: string[]) => [...source].sort((a, b) => b.length - a.length)
// App config is cloned per request on the server, but reactive and updated in place on the client (HMR, `updateAppConfig`)
sorted = import.meta.client ? computed(() => sort(reactive(key))) : { value: sort(key) }
sortedCollectionsCache.set(key, sorted)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
return sorted.value
}

export function resolveName(name: string, options: Pick<NuxtIconRuntimeOptions, 'cssSelectorPrefix' | 'aliases' | 'collections'>): string {
const bare = name.startsWith(options.cssSelectorPrefix)
? name.slice(options.cssSelectorPrefix.length)
: name
const resolved = options.aliases?.[bare] || bare

// Disambiguate collection
// e.g. `simple-icons-github` -> `simple-icons:github`
if (!resolved.includes(':')) {
const collection = getSortedCollections(options.collections)
.find(c => resolved.charCodeAt(c.length) === HYPHEN && resolved.startsWith(c))
return collection
? collection + ':' + resolved.slice(collection.length + 1)
: resolved
}
return resolved
}
20 changes: 2 additions & 18 deletions src/runtime/components/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { IconifyIcon } from '@iconify/types'
import type { NuxtIconRuntimeOptions } from '../../types'
import { useAppConfig } from '#imports'
import { init as initClientBundle } from '#build/nuxt-icon-client-bundle'
import { resolveName } from './resolve-name'

export { initClientBundle }

Expand Down Expand Up @@ -47,25 +48,8 @@ export async function loadIcon(name: string, timeout: number): Promise<Required<

export function useResolvedName(getName: () => string) {
const options = useAppConfig().icon as NuxtIconRuntimeOptions
const collections = (options.collections || []).sort((a, b) => b.length - a.length)

return computed(() => {
const name = getName()
const bare = name.startsWith(options.cssSelectorPrefix)
? name.slice(options.cssSelectorPrefix.length)
: name
const resolved = options.aliases?.[bare] || bare

// Disambiguate collection
// e.g. `simple-icons-github` -> `simple-icons:github`
if (!resolved.includes(':')) {
const collection = collections.find(c => resolved.startsWith(c + '-'))
return collection
? collection + ':' + resolved.slice(collection.length + 1)
: resolved
}
return resolved
})
return computed(() => resolveName(getName(), options))
}

export function resolveCustomizeFn(
Expand Down
82 changes: 82 additions & 0 deletions test/resolve-name.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { computed, reactive } from 'vue'
import { describe, expect, it } from 'vitest'
import { getSortedCollections, resolveName } from '../src/runtime/components/resolve-name'

function createOptions(collections: string[] | null, aliases: Record<string, string> = {}) {
return { cssSelectorPrefix: 'i-', aliases, collections }
}

describe('resolveName', () => {
it('keeps names that already contain a collection separator', () => {
expect(resolveName('i-simple-icons:github', createOptions(['simple-icons']))).toBe('simple-icons:github')
})

it('resolves a bare name against the longest matching collection', () => {
const options = createOptions(['simple', 'simple-icons', 'si'])
expect(resolveName('i-simple-icons-github', options)).toBe('simple-icons:github')
expect(resolveName('simple-icons-github', options)).toBe('simple-icons:github')
expect(resolveName('i-simple-foo', options)).toBe('simple:foo')
expect(resolveName('i-si-foo', options)).toBe('si:foo')
})

it('requires a hyphen after the collection name', () => {
const options = createOptions(['simple', 'si'])
expect(resolveName('i-simpleicons', options)).toBe('simpleicons')
expect(resolveName('i-si', options)).toBe('si')
})

it('resolves aliases before matching collections', () => {
expect(resolveName('i-github', createOptions(['simple-icons'], { github: 'simple-icons-github' }))).toBe('simple-icons:github')
})

it('keeps bare names without a matching collection', () => {
expect(resolveName('i-unknown-icon', createOptions(['simple-icons']))).toBe('unknown-icon')
expect(resolveName('i-unknown-icon', createOptions(null))).toBe('unknown-icon')
})

it('does not reorder the configured collections', () => {
const collections = ['simple', 'simple-icons', 'si']
resolveName('i-simple-icons-github', createOptions(collections))
expect(collections).toEqual(['simple', 'simple-icons', 'si'])
})

it('updates computed names when reactive collections change in place', () => {
const options = reactive(createOptions(['simple', 'foo']))
const first = computed(() => resolveName('i-simple-icons-github', options))
const second = computed(() => resolveName('i-simple-icons-github', options))
expect(first.value).toBe('simple:icons-github')
expect(second.value).toBe('simple:icons-github')

options.collections![1] = 'simple-icons'
expect(first.value).toBe('simple-icons:github')
expect(second.value).toBe('simple-icons:github')
})

it('updates computed names when collections first resolved as a plain array change', () => {
const collections = ['simple', 'foo']
expect(resolveName('i-simple-icons-github', createOptions(collections))).toBe('simple:icons-github')

const options = reactive(createOptions(collections))
const name = computed(() => resolveName('i-simple-icons-github', options))
expect(name.value).toBe('simple:icons-github')

options.collections![1] = 'simple-icons'
expect(name.value).toBe('simple-icons:github')
})

it('updates computed names when reactive collections grow', () => {
const options = reactive(createOptions(['simple']))
const name = computed(() => resolveName('i-simple-icons-github', options))
expect(name.value).toBe('simple:icons-github')

options.collections!.push('simple-icons')
expect(name.value).toBe('simple-icons:github')
})
})

describe('getSortedCollections', () => {
it('sorts each collections array once', () => {
const collections = ['simple', 'simple-icons']
expect(getSortedCollections(collections)).toBe(getSortedCollections(collections))
})
})
3 changes: 3 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
define: {
'import.meta.client': 'true',
},
test: {
exclude: ['playgrounds/**', 'node_modules/**'],
},
Expand Down
Loading