From b4e1135b15f8f12a3f83584a7ccb653126bdd9dc Mon Sep 17 00:00:00 2001 From: zernonia Date: Wed, 27 Nov 2024 11:10:34 +0800 Subject: [PATCH] feat: transform icons --- packages/cli/src/utils/transformers/index.ts | 5 ++ .../src/utils/transformers/transform-icons.ts | 75 +++++++++++++++++++ .../cli/src/utils/updaters/update-files.ts | 51 +++++++++---- .../transform-icons.test.ts.snap | 27 +++++++ .../cli/test/utils/transform-icons.test.ts | 44 +++++++++++ 5 files changed, 189 insertions(+), 13 deletions(-) create mode 100644 packages/cli/src/utils/transformers/transform-icons.ts create mode 100644 packages/cli/test/utils/__snapshots__/transform-icons.test.ts.snap create mode 100644 packages/cli/test/utils/transform-icons.test.ts diff --git a/packages/cli/src/utils/transformers/index.ts b/packages/cli/src/utils/transformers/index.ts index 98ffcd01..365ab463 100644 --- a/packages/cli/src/utils/transformers/index.ts +++ b/packages/cli/src/utils/transformers/index.ts @@ -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 } diff --git a/packages/cli/src/utils/transformers/transform-icons.ts b/packages/cli/src/utils/transformers/transform-icons.ts new file mode 100644 index 00000000..efa2df44 --- /dev/null +++ b/packages/cli/src/utils/transformers/transform-icons.ts @@ -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>): 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 + const targetedIconsMap: Map = 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 + }, + } +} diff --git a/packages/cli/src/utils/updaters/update-files.ts b/packages/cli/src/utils/updaters/update-files.ts index 279174cd..97631355 100644 --- a/packages/cli/src/utils/updaters/update-files.ts +++ b/packages/cli/src/utils/updaters/update-files.ts @@ -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() 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. diff --git a/packages/cli/test/utils/__snapshots__/transform-icons.test.ts.snap b/packages/cli/test/utils/__snapshots__/transform-icons.test.ts.snap new file mode 100644 index 00000000..d159155c --- /dev/null +++ b/packages/cli/test/utils/__snapshots__/transform-icons.test.ts.snap @@ -0,0 +1,27 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`transformIcons > does not transform lucide icons 1`] = ` +" + + +" +`; + +exports[`transformIcons > transforms radix icons 1`] = ` +" + + +" +`; diff --git a/packages/cli/test/utils/transform-icons.test.ts b/packages/cli/test/utils/transform-icons.test.ts new file mode 100644 index 00000000..03c6a42d --- /dev/null +++ b/packages/cli/test/utils/transform-icons.test.ts @@ -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: ` + + + `, + config: { + iconLibrary: 'radix', + }, + }) + expect(result).toMatchSnapshot() + }) + + it('does not transform lucide icons', async () => { + const result = await transform({ + filename: 'app.vue', + raw: ` + + + `, + config: { + iconLibrary: 'lucide', + }, + }) + expect(result).toMatchSnapshot() + }) +})