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 { registryBaseColorSchema } from '@/src/utils/registry/schema'
|
||||
import type * as z from 'zod'
|
||||
import { getRegistryIcons } from '@/src/utils/registry'
|
||||
import { transformCssVars } from '@/src/utils/transformers/transform-css-vars'
|
||||
import { transformImport } from '@/src/utils/transformers/transform-import'
|
||||
import { transformSFC } from '@/src/utils/transformers/transform-sfc'
|
||||
import { transformTwPrefix } from '@/src/utils/transformers/transform-tw-prefix'
|
||||
import { transform as metaTransform } from 'vue-metamorph'
|
||||
import { transformIcons } from './transform-icons'
|
||||
|
||||
export interface TransformOpts {
|
||||
filename: string
|
||||
|
|
@ -17,9 +19,12 @@ export interface TransformOpts {
|
|||
export async function transform(opts: TransformOpts) {
|
||||
const source = await transformSFC(opts)
|
||||
|
||||
const registryIcons = await getRegistryIcons()
|
||||
|
||||
return metaTransform(source, opts.filename, [
|
||||
transformImport(opts),
|
||||
transformCssVars(opts),
|
||||
transformTwPrefix(opts),
|
||||
transformIcons(opts, registryIcons),
|
||||
]).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 { RegistryItem } from '@/src/utils/registry/schema'
|
||||
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 { highlighter } from '@/src/utils/highlighter'
|
||||
import { logger } from '@/src/utils/logger'
|
||||
|
|
@ -57,6 +57,7 @@ export async function updateFiles(
|
|||
|
||||
const filesCreated = []
|
||||
const filesUpdated = []
|
||||
const folderSkipped = new Map<string, boolean>()
|
||||
const filesSkipped = []
|
||||
|
||||
for (const file of files) {
|
||||
|
|
@ -78,22 +79,46 @@ export async function updateFiles(
|
|||
}
|
||||
|
||||
const existingFile = existsSync(filePath)
|
||||
if (existingFile && !options.overwrite) {
|
||||
filesCreatedSpinner.stop()
|
||||
const { overwrite } = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'overwrite',
|
||||
message: `The file ${highlighter.info(
|
||||
fileName,
|
||||
)} already exists. Would you like to overwrite?`,
|
||||
initial: false,
|
||||
})
|
||||
|
||||
if (!overwrite) {
|
||||
// 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
|
||||
}
|
||||
filesCreatedSpinner?.start()
|
||||
}
|
||||
else {
|
||||
if (existingFile && !options.overwrite) {
|
||||
filesCreatedSpinner.stop()
|
||||
const { overwrite } = await prompts({
|
||||
type: 'confirm',
|
||||
name: 'overwrite',
|
||||
message: `The file ${highlighter.info(
|
||||
fileName,
|
||||
)} already exists. Would you like to overwrite?`,
|
||||
initial: false,
|
||||
})
|
||||
|
||||
if (!overwrite) {
|
||||
filesSkipped.push(path.relative(config.resolvedPaths.cwd, filePath))
|
||||
continue
|
||||
}
|
||||
filesCreatedSpinner?.start()
|
||||
}
|
||||
}
|
||||
|
||||
// Create the target directory if it doesn't exist.
|
||||
|
|
|
|||
|
|
@ -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