Skip to content

Commit

Permalink
feat: transform icons
Browse files Browse the repository at this point in the history
  • Loading branch information
zernonia committed Nov 27, 2024
1 parent b3e10e4 commit b4e1135
Show file tree
Hide file tree
Showing 5 changed files with 190 additions and 14 deletions.
5 changes: 5 additions & 0 deletions packages/cli/src/utils/transformers/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 changes: 75 additions & 0 deletions packages/cli/src/utils/transformers/transform-icons.ts
Original file line number Diff line number Diff line change
@@ -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
},
}
}
53 changes: 39 additions & 14 deletions packages/cli/src/utils/updaters/update-files.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions packages/cli/test/utils/__snapshots__/transform-icons.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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 changes: 44 additions & 0 deletions packages/cli/test/utils/transform-icons.test.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})

0 comments on commit b4e1135

Please sign in to comment.