feat: transform icons
This commit is contained in:
parent
b3e10e480f
commit
b4e1135b15
|
|
@ -1,11 +1,13 @@
|
||||||
import type { Config } from '@/src/utils/get-config'
|
import type { Config } from '@/src/utils/get-config'
|
||||||
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
import type { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
||||||
import type * as z from 'zod'
|
import type * as z from 'zod'
|
||||||
|
import { getRegistryIcons } from '@/src/utils/registry'
|
||||||
import { transformCssVars } from '@/src/utils/transformers/transform-css-vars'
|
import { transformCssVars } from '@/src/utils/transformers/transform-css-vars'
|
||||||
import { transformImport } from '@/src/utils/transformers/transform-import'
|
import { transformImport } from '@/src/utils/transformers/transform-import'
|
||||||
import { transformSFC } from '@/src/utils/transformers/transform-sfc'
|
import { transformSFC } from '@/src/utils/transformers/transform-sfc'
|
||||||
import { transformTwPrefix } from '@/src/utils/transformers/transform-tw-prefix'
|
import { transformTwPrefix } from '@/src/utils/transformers/transform-tw-prefix'
|
||||||
import { transform as metaTransform } from 'vue-metamorph'
|
import { transform as metaTransform } from 'vue-metamorph'
|
||||||
|
import { transformIcons } from './transform-icons'
|
||||||
|
|
||||||
export interface TransformOpts {
|
export interface TransformOpts {
|
||||||
filename: string
|
filename: string
|
||||||
|
|
@ -17,9 +19,12 @@ export interface TransformOpts {
|
||||||
export async function transform(opts: TransformOpts) {
|
export async function transform(opts: TransformOpts) {
|
||||||
const source = await transformSFC(opts)
|
const source = await transformSFC(opts)
|
||||||
|
|
||||||
|
const registryIcons = await getRegistryIcons()
|
||||||
|
|
||||||
return metaTransform(source, opts.filename, [
|
return metaTransform(source, opts.filename, [
|
||||||
transformImport(opts),
|
transformImport(opts),
|
||||||
transformCssVars(opts),
|
transformCssVars(opts),
|
||||||
transformTwPrefix(opts),
|
transformTwPrefix(opts),
|
||||||
|
transformIcons(opts, registryIcons),
|
||||||
]).code
|
]).code
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
packages/cli/src/utils/transformers/transform-icons.ts
Normal file
75
packages/cli/src/utils/transformers/transform-icons.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import type { CodemodPlugin } from 'vue-metamorph'
|
||||||
|
import type { TransformOpts } from '.'
|
||||||
|
import { ICON_LIBRARIES } from '@/src/utils/icon-libraries'
|
||||||
|
|
||||||
|
// Lucide is the default icon library in the registry.
|
||||||
|
const SOURCE_LIBRARY = 'lucide'
|
||||||
|
|
||||||
|
export function transformIcons(opts: TransformOpts, registryIcons: Record<string, Record<string, string>>): CodemodPlugin {
|
||||||
|
return {
|
||||||
|
type: 'codemod',
|
||||||
|
name: 'modify import of icon library on user config',
|
||||||
|
|
||||||
|
transform({ scriptASTs, sfcAST, utils: { traverseScriptAST, traverseTemplateAST } }) {
|
||||||
|
let transformCount = 0
|
||||||
|
const { config } = opts
|
||||||
|
|
||||||
|
// No transform if we cannot read the icon library.
|
||||||
|
if (!config.iconLibrary || !(config.iconLibrary in ICON_LIBRARIES)) {
|
||||||
|
return transformCount
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceLibrary = SOURCE_LIBRARY
|
||||||
|
const targetLibrary = config.iconLibrary
|
||||||
|
|
||||||
|
if (sourceLibrary === targetLibrary) {
|
||||||
|
return transformCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map<orignalIcon, targetedIcon>
|
||||||
|
const targetedIconsMap: Map<string, string> = new Map()
|
||||||
|
for (const scriptAST of scriptASTs) {
|
||||||
|
traverseScriptAST(scriptAST, {
|
||||||
|
|
||||||
|
visitImportDeclaration(path) {
|
||||||
|
if (![ICON_LIBRARIES.radix.import, ICON_LIBRARIES.lucide.import].includes(`${path.node.source.value}`))
|
||||||
|
return this.traverse(path)
|
||||||
|
|
||||||
|
for (const specifier of path.node.specifiers ?? []) {
|
||||||
|
if (specifier.type === 'ImportSpecifier') {
|
||||||
|
const iconName = specifier.imported.name
|
||||||
|
|
||||||
|
const targetedIcon = registryIcons[iconName]?.[targetLibrary]
|
||||||
|
|
||||||
|
if (!targetedIcon || targetedIconsMap.has(targetedIcon)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
targetedIconsMap.set(iconName, targetedIcon)
|
||||||
|
specifier.imported.name = targetedIcon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetedIconsMap.size > 0)
|
||||||
|
path.node.source.value = ICON_LIBRARIES[targetLibrary as keyof typeof ICON_LIBRARIES].import
|
||||||
|
|
||||||
|
return this.traverse(path)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (sfcAST) {
|
||||||
|
traverseTemplateAST(sfcAST, {
|
||||||
|
enterNode(node) {
|
||||||
|
if (node.type === 'VElement' && targetedIconsMap.has(node.rawName)) {
|
||||||
|
node.rawName = targetedIconsMap.get(node.rawName) ?? ''
|
||||||
|
transformCount++
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transformCount
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { Config } from '@/src/utils/get-config'
|
import type { Config } from '@/src/utils/get-config'
|
||||||
import type { RegistryItem } from '@/src/utils/registry/schema'
|
import type { RegistryItem } from '@/src/utils/registry/schema'
|
||||||
import { existsSync, promises as fs } from 'node:fs'
|
import { existsSync, promises as fs } from 'node:fs'
|
||||||
import path, { basename } from 'node:path'
|
import path, { basename, dirname } from 'node:path'
|
||||||
import { getProjectInfo } from '@/src/utils/get-project-info'
|
import { getProjectInfo } from '@/src/utils/get-project-info'
|
||||||
import { highlighter } from '@/src/utils/highlighter'
|
import { highlighter } from '@/src/utils/highlighter'
|
||||||
import { logger } from '@/src/utils/logger'
|
import { logger } from '@/src/utils/logger'
|
||||||
|
|
@ -57,6 +57,7 @@ export async function updateFiles(
|
||||||
|
|
||||||
const filesCreated = []
|
const filesCreated = []
|
||||||
const filesUpdated = []
|
const filesUpdated = []
|
||||||
|
const folderSkipped = new Map<string, boolean>()
|
||||||
const filesSkipped = []
|
const filesSkipped = []
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
|
|
@ -78,6 +79,29 @@ export async function updateFiles(
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingFile = existsSync(filePath)
|
const existingFile = existsSync(filePath)
|
||||||
|
|
||||||
|
// Check for existing folder in UI component only
|
||||||
|
if (file.type === 'registry:ui') {
|
||||||
|
const folderName = basename(dirname(filePath))
|
||||||
|
|
||||||
|
if (!folderSkipped.has(folderName)) {
|
||||||
|
filesCreatedSpinner.stop()
|
||||||
|
const { overwrite } = await prompts({
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'overwrite',
|
||||||
|
message: `The folder ${highlighter.info(folderName)} already exists. Would you like to overwrite?`,
|
||||||
|
initial: false,
|
||||||
|
})
|
||||||
|
folderSkipped.set(folderName, !overwrite)
|
||||||
|
filesCreatedSpinner?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (folderSkipped.get(folderName) === true) {
|
||||||
|
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
if (existingFile && !options.overwrite) {
|
if (existingFile && !options.overwrite) {
|
||||||
filesCreatedSpinner.stop()
|
filesCreatedSpinner.stop()
|
||||||
const { overwrite } = await prompts({
|
const { overwrite } = await prompts({
|
||||||
|
|
@ -95,6 +119,7 @@ export async function updateFiles(
|
||||||
}
|
}
|
||||||
filesCreatedSpinner?.start()
|
filesCreatedSpinner?.start()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create the target directory if it doesn't exist.
|
// Create the target directory if it doesn't exist.
|
||||||
if (!existsSync(targetDir)) {
|
if (!existsSync(targetDir)) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`transformIcons > does not transform lucide icons 1`] = `
|
||||||
|
"<script setup>
|
||||||
|
import { Check } from 'lucide-vue-next';
|
||||||
|
import { Primitive } from 'reka-ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Check />
|
||||||
|
<Primitive />
|
||||||
|
</template>
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`transformIcons > transforms radix icons 1`] = `
|
||||||
|
"<script setup>
|
||||||
|
import { CheckIcon } from '@radix-icons/vue';
|
||||||
|
import { Primitive } from 'reka-ui';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CheckIcon />
|
||||||
|
<Primitive />
|
||||||
|
</template>
|
||||||
|
"
|
||||||
|
`;
|
||||||
44
packages/cli/test/utils/transform-icons.test.ts
Normal file
44
packages/cli/test/utils/transform-icons.test.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { transform } from '../../src/utils/transformers'
|
||||||
|
|
||||||
|
describe('transformIcons', () => {
|
||||||
|
it('transforms radix icons', async () => {
|
||||||
|
const result = await transform({
|
||||||
|
filename: 'app.vue',
|
||||||
|
raw: `<script lang="ts" setup>
|
||||||
|
import { Check } from 'lucide-vue-next'
|
||||||
|
import { Primitive } from 'reka-ui'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Check />
|
||||||
|
<Primitive />
|
||||||
|
</template>
|
||||||
|
`,
|
||||||
|
config: {
|
||||||
|
iconLibrary: 'radix',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(result).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not transform lucide icons', async () => {
|
||||||
|
const result = await transform({
|
||||||
|
filename: 'app.vue',
|
||||||
|
raw: `<script lang="ts" setup>
|
||||||
|
import { Check } from 'lucide-vue-next'
|
||||||
|
import { Primitive } from 'reka-ui'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Check />
|
||||||
|
<Primitive />
|
||||||
|
</template>
|
||||||
|
`,
|
||||||
|
config: {
|
||||||
|
iconLibrary: 'lucide',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(result).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user