Skip to content

Commit

Permalink
feat(shadcn): implement icons transformer
Browse files Browse the repository at this point in the history
  • Loading branch information
shadcn committed Nov 4, 2024
1 parent f09e370 commit 990b9d2
Show file tree
Hide file tree
Showing 14 changed files with 145 additions and 27 deletions.
3 changes: 3 additions & 0 deletions apps/www/public/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
"tsx": {
"type": "boolean"
},
"iconLibrary": {
"type": "string"
},
"aliases": {
"type": "object",
"properties": {
Expand Down
20 changes: 20 additions & 0 deletions packages/shadcn/src/commands/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getConfig } from "@/src/utils/get-config"
import { getProjectInfo } from "@/src/utils/get-project-info"
import { logger } from "@/src/utils/logger"
import { Command } from "commander"

export const info = new Command()
.name("info")
.description("get information about your project")
.option(
"-c, --cwd <cwd>",
"the working directory. defaults to the current directory.",
process.cwd()
)
.action(async (opts) => {
logger.info("> project info")
console.log(await getProjectInfo(opts.cwd))
logger.break()
logger.info("> components.json")
console.log(await getConfig(opts.cwd))
})
1 change: 1 addition & 0 deletions packages/shadcn/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,5 +333,6 @@ async function promptForMinimalConfig(
rsc: defaultConfig?.rsc,
tsx: defaultConfig?.tsx,
aliases: defaultConfig?.aliases,
iconLibrary: defaultConfig?.iconLibrary,
})
}
8 changes: 7 additions & 1 deletion packages/shadcn/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { add } from "@/src/commands/add"
import { diff } from "@/src/commands/diff"
import { info } from "@/src/commands/info"
import { init } from "@/src/commands/init"
import { migrate } from "@/src/commands/migrate"
import { Command } from "commander"
Expand All @@ -20,7 +21,12 @@ async function main() {
"display the version number"
)

program.addCommand(init).addCommand(add).addCommand(diff).addCommand(migrate)
program
.addCommand(init)
.addCommand(add)
.addCommand(diff)
.addCommand(migrate)
.addCommand(info)

program.parse()
}
Expand Down
37 changes: 15 additions & 22 deletions packages/shadcn/src/migrations/migrate-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tmpdir } from "os"
import path from "path"
import { Config } from "@/src/utils/get-config"
import { highlighter } from "@/src/utils/highlighter"
import { ICON_LIBRARIES } from "@/src/utils/icon-libraries"
import { logger } from "@/src/utils/logger"
import { getRegistryIcons } from "@/src/utils/registry"
import { iconsSchema } from "@/src/utils/registry/schema"
Expand All @@ -14,11 +15,6 @@ import { Project, ScriptKind, SyntaxKind } from "ts-morph"
import { PackageJson } from "type-fest"
import { z } from "zod"

export const iconLibraries = {
lucide: "lucide-react",
radix: "@radix-ui/react-icons",
}

export async function migrateIcons(config: Config) {
if (!config.resolvedPaths.ui) {
throw new Error(
Expand All @@ -38,7 +34,7 @@ export async function migrateIcons(config: Config) {
throw new Error("Something went wrong fetching the registry icons.")
}

const libraryChoices = Object.entries(iconLibraries).map(
const libraryChoices = Object.entries(ICON_LIBRARIES).map(
([name, packageName]) => ({
title: packageName,
value: name,
Expand Down Expand Up @@ -74,17 +70,17 @@ export async function migrateIcons(config: Config) {

if (
!(
migrateOptions.sourceLibrary in iconLibraries &&
migrateOptions.targetLibrary in iconLibraries
migrateOptions.sourceLibrary in ICON_LIBRARIES &&
migrateOptions.targetLibrary in ICON_LIBRARIES
)
) {
throw new Error("Invalid icon library. Please choose a valid icon library.")
}

const sourceLibrary =
iconLibraries[migrateOptions.sourceLibrary as keyof typeof iconLibraries]
ICON_LIBRARIES[migrateOptions.sourceLibrary as keyof typeof ICON_LIBRARIES]
const targetLibrary =
iconLibraries[migrateOptions.targetLibrary as keyof typeof iconLibraries]
ICON_LIBRARIES[migrateOptions.targetLibrary as keyof typeof ICON_LIBRARIES]

const { confirm } = await prompts({
type: "confirm",
Expand Down Expand Up @@ -129,12 +125,12 @@ export async function migrateIcons(config: Config) {

export async function migrateIconsFile(
content: string,
sourceLibrary: keyof typeof iconLibraries,
targetLibrary: keyof typeof iconLibraries,
sourceLibrary: keyof typeof ICON_LIBRARIES,
targetLibrary: keyof typeof ICON_LIBRARIES,
iconsMapping: z.infer<typeof iconsSchema>
) {
const sourceLibraryName = iconLibraries[sourceLibrary]
const targetLibraryName = iconLibraries[targetLibrary]
const sourceLibraryName = ICON_LIBRARIES[sourceLibrary]
const targetLibraryName = ICON_LIBRARIES[targetLibrary]

const dir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-"))
const project = new Project({
Expand Down Expand Up @@ -177,13 +173,10 @@ export async function migrateIconsFile(
specifier.remove()

// Replace with the targeted icon.
const jsxElements = sourceFile.getDescendantsOfKind(
SyntaxKind.JsxSelfClosingElement
)
const iconElement = jsxElements.find(
(node) => node.getTagNameNode()?.getText() === iconName
)
iconElement?.getTagNameNode()?.replaceWithText(targetedIcon)
sourceFile
.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
.filter((node) => node.getTagNameNode()?.getText() === iconName)
.forEach((node) => node.getTagNameNode()?.replaceWithText(targetedIcon))
}

// If the named import is empty, remove the import declaration.
Expand All @@ -205,7 +198,7 @@ export async function migrateIconsFile(
}

function _getIconLibraries(packageInfo: PackageJson) {
return Object.values(iconLibraries).filter(
return Object.values(ICON_LIBRARIES).filter(
(library) =>
packageInfo.dependencies?.[library] ||
packageInfo.devDependencies?.[library]
Expand Down
6 changes: 6 additions & 0 deletions packages/shadcn/src/utils/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const rawConfigSchema = z
lib: z.string().optional(),
hooks: z.string().optional(),
}),
iconLibrary: z.string().optional(),
})
.strict()

Expand Down Expand Up @@ -65,6 +66,11 @@ export async function getConfig(cwd: string) {
return null
}

// Set default icon library if not provided.
if (!config.iconLibrary) {
config.iconLibrary = config.style === "new-york" ? "radix" : "lucide"
}

return await resolveConfigPaths(cwd, config)
}

Expand Down
1 change: 1 addition & 0 deletions packages/shadcn/src/utils/get-project-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ export async function getProjectConfig(
cssVariables: true,
prefix: "",
},
iconLibrary: "lucide",
aliases: {
components: `${projectInfo.aliasPrefix}/components`,
ui: `${projectInfo.aliasPrefix}/components/ui`,
Expand Down
4 changes: 4 additions & 0 deletions packages/shadcn/src/utils/icon-libraries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const ICON_LIBRARIES = {
lucide: "lucide-react",
radix: "@radix-ui/react-icons",
}
4 changes: 3 additions & 1 deletion packages/shadcn/src/utils/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from "path"
import { Config } from "@/src/utils/get-config"
import { registryBaseColorSchema } from "@/src/utils/registry/schema"
import { transformCssVars } from "@/src/utils/transformers/transform-css-vars"
import { transformIcons } from "@/src/utils/transformers/transform-icons"
import { transformImport } from "@/src/utils/transformers/transform-import"
import { transformJsx } from "@/src/utils/transformers/transform-jsx"
import { transformRsc } from "@/src/utils/transformers/transform-rsc"
Expand Down Expand Up @@ -42,6 +43,7 @@ export async function transform(
transformRsc,
transformCssVars,
transformTwPrefixes,
transformIcons,
]
) {
const tempFile = await createTempSourceFile(opts.filename)
Expand All @@ -50,7 +52,7 @@ export async function transform(
})

for (const transformer of transformers) {
transformer({ sourceFile, ...opts })
await transformer({ sourceFile, ...opts })
}

if (opts.transformJsx) {
Expand Down
70 changes: 70 additions & 0 deletions packages/shadcn/src/utils/transformers/transform-icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { ICON_LIBRARIES } from "@/src/utils/icon-libraries"
import { getRegistryIcons } from "@/src/utils/registry"
import { Transformer } from "@/src/utils/transformers"
import { SyntaxKind } from "ts-morph"

// Lucide is the default icon library in the registry.
const SOURCE_LIBRARY = "lucide"

export const transformIcons: Transformer = async ({ sourceFile, config }) => {
// No transform if we cannot read the icon library.
if (!config.iconLibrary || !(config.iconLibrary in ICON_LIBRARIES)) {
return sourceFile
}

const registryIcons = await getRegistryIcons()
const sourceLibrary = SOURCE_LIBRARY
const targetLibrary = config.iconLibrary

if (sourceLibrary === targetLibrary) {
return sourceFile
}

let targetedIcons: string[] = []
for (const importDeclaration of sourceFile.getImportDeclarations() ?? []) {
if (
importDeclaration.getModuleSpecifier()?.getText() !==
`"${ICON_LIBRARIES[SOURCE_LIBRARY]}"`
) {
continue
}

for (const specifier of importDeclaration.getNamedImports() ?? []) {
const iconName = specifier.getName()

const targetedIcon = registryIcons[iconName][targetLibrary]

if (!targetedIcon) {
continue
}

targetedIcons.push(targetedIcon)

// Remove the named import.
specifier.remove()

// Replace with the targeted icon.
sourceFile
.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement)
.filter((node) => node.getTagNameNode()?.getText() === iconName)
.forEach((node) => node.getTagNameNode()?.replaceWithText(targetedIcon))
}

// If the named import is empty, remove the import declaration.
if (importDeclaration.getNamedImports()?.length === 0) {
importDeclaration.remove()
}
}

if (targetedIcons.length > 0) {
sourceFile.addImportDeclaration({
moduleSpecifier:
ICON_LIBRARIES[targetLibrary as keyof typeof ICON_LIBRARIES],
namedImports: targetedIcons.map((icon) => ({
name: icon,
})),
})
}

return sourceFile
}
9 changes: 8 additions & 1 deletion packages/shadcn/src/utils/updaters/update-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RegistryItem } from "@/src/utils/registry/schema"
import { spinner } from "@/src/utils/spinner"
import { transform } from "@/src/utils/transformers"
import { transformCssVars } from "@/src/utils/transformers/transform-css-vars"
import { transformIcons } from "@/src/utils/transformers/transform-icons"
import { transformImport } from "@/src/utils/transformers/transform-import"
import { transformRsc } from "@/src/utils/transformers/transform-rsc"
import { transformTwPrefixes } from "@/src/utils/transformers/transform-tw-prefix"
Expand Down Expand Up @@ -114,7 +115,13 @@ export async function updateFiles(
baseColor,
transformJsx: !config.tsx,
},
[transformImport, transformRsc, transformCssVars, transformTwPrefixes]
[
transformImport,
transformRsc,
transformCssVars,
transformTwPrefixes,
transformIcons,
]
)

await fs.writeFile(filePath, content, "utf-8")
Expand Down
3 changes: 2 additions & 1 deletion packages/shadcn/test/fixtures/config-full/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@
"lib": "~/lib",
"hooks": "~/lib/hooks",
"ui": "~/ui"
}
},
"iconLibrary": "lucide"
}
3 changes: 2 additions & 1 deletion packages/shadcn/test/fixtures/config-jsx/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"aliases": {
"utils": "@/lib/utils",
"components": "@/components"
}
},
"iconLibrary": "radix"
}
3 changes: 3 additions & 0 deletions packages/shadcn/test/utils/get-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ test("get config", async () => {
hooks: path.resolve(__dirname, "../fixtures/config-partial", "./hooks"),
lib: path.resolve(__dirname, "../fixtures/config-partial", "./lib"),
},
iconLibrary: "lucide",
})

expect(
Expand All @@ -108,6 +109,7 @@ test("get config", async () => {
hooks: "~/lib/hooks",
ui: "~/ui",
},
iconLibrary: "lucide",
resolvedPaths: {
cwd: path.resolve(__dirname, "../fixtures/config-full"),
tailwindConfig: path.resolve(
Expand Down Expand Up @@ -156,6 +158,7 @@ test("get config", async () => {
components: "@/components",
utils: "@/lib/utils",
},
iconLibrary: "radix",
resolvedPaths: {
cwd: path.resolve(__dirname, "../fixtures/config-jsx"),
tailwindConfig: path.resolve(
Expand Down

0 comments on commit 990b9d2

Please sign in to comment.