Files
shadcn-ui/apps/v4/lib/registry.ts
shadcn 86d9b00084 chore: update deps (#9022)
* feat: init

* fix

* fix

* fix

* feat

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: implement icons

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: update init command

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: dialog

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add registry:base item type

* feat: rename frame to canva

* fix

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fi

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add all colors

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add outfit font

* fix

* fix

* fix

* fix

* fix

* chore: changeset

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2025-12-12 21:01:44 +04:00

238 lines
5.7 KiB
TypeScript

import { promises as fs } from "fs"
import { tmpdir } from "os"
import path from "path"
import { registryItemSchema, type registryItemFileSchema } from "shadcn/schema"
import { Project, ScriptKind } from "ts-morph"
import { type z } from "zod"
import { Index } from "@/registry/__index__"
import { type Style } from "@/registry/_legacy-styles"
export function getRegistryComponent(name: string, styleName: Style["name"]) {
return Index[styleName]?.[name]?.component
}
export async function getRegistryItems(
styleName: Style["name"],
filter?: (item: z.infer<typeof registryItemSchema>) => boolean
) {
const styleIndex = Index[styleName]
if (!styleIndex) {
return []
}
const entries = Object.values(styleIndex)
const filteredEntries = filter ? entries.filter(filter) : entries
return await Promise.all(
filteredEntries.map(async (entry) => {
const item = await getRegistryItem(entry.name, styleName)
return item
})
).then((results) => results.filter(Boolean))
}
export async function getRegistryItem(name: string, styleName: Style["name"]) {
const item = Index[styleName]?.[name]
if (!item) {
return null
}
// Convert all file paths to object.
// TODO: remove when we migrate to new registry.
item.files = item.files.map((file: unknown) =>
typeof file === "string" ? { path: file } : file
)
// Fail early before doing expensive file operations.
const result = registryItemSchema.safeParse(item)
if (!result.success) {
return null
}
let files: typeof result.data.files = []
for (const file of item.files) {
const content = await getFileContent(file)
const relativePath = path.relative(process.cwd(), file.path)
files.push({
...file,
path: relativePath,
content,
})
}
// Fix file paths.
files = fixFilePaths(files)
const parsed = registryItemSchema.safeParse({
...result.data,
files,
})
if (!parsed.success) {
console.error(parsed.error.message)
return null
}
return parsed.data
}
async function getFileContent(file: z.infer<typeof registryItemFileSchema>) {
const raw = await fs.readFile(file.path, "utf-8")
const project = new Project({
compilerOptions: {},
})
const tempFile = await createTempSourceFile(file.path)
const sourceFile = project.createSourceFile(tempFile, raw, {
scriptKind: ScriptKind.TSX,
})
// Remove meta variables.
// removeVariable(sourceFile, "iframeHeight")
// removeVariable(sourceFile, "containerClassName")
// removeVariable(sourceFile, "description")
let code = sourceFile.getFullText()
// Some registry items uses default export.
// We want to use named export instead.
// TODO: do we really need this? - @shadcn.
if (file.type !== "registry:page") {
code = code.replaceAll("export default", "export")
}
// Fix imports.
code = fixImport(code)
return code
}
function getFileTarget(file: z.infer<typeof registryItemFileSchema>) {
let target = file.target
if (!target || target === "") {
const fileName = file.path.split("/").pop()
if (
file.type === "registry:block" ||
file.type === "registry:component" ||
file.type === "registry:example"
) {
target = `components/${fileName}`
}
if (file.type === "registry:ui") {
target = `components/ui/${fileName}`
}
if (file.type === "registry:hook") {
target = `hooks/${fileName}`
}
if (file.type === "registry:lib") {
target = `lib/${fileName}`
}
}
return target ?? ""
}
async function createTempSourceFile(filename: string) {
const dir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-"))
return path.join(dir, filename)
}
function fixFilePaths(files: z.infer<typeof registryItemSchema>["files"]) {
if (!files) {
return []
}
// Resolve all paths relative to the first file's directory.
const firstFilePath = files[0].path
const firstFilePathDir = path.dirname(firstFilePath)
return files.map((file) => {
return {
...file,
path: path.relative(firstFilePathDir, file.path),
target: getFileTarget(file),
}
})
}
export function fixImport(content: string) {
const regex = /@\/(.+?)\/((?:.*?\/)?(?:components|ui|hooks|lib))\/([\w-]+)/g
const replacement = (
match: string,
path: string,
type: string,
component: string
) => {
if (type.endsWith("components")) {
return `@/components/${component}`
} else if (type.endsWith("ui")) {
return `@/components/ui/${component}`
} else if (type.endsWith("hooks")) {
return `@/hooks/${component}`
} else if (type.endsWith("lib")) {
return `@/lib/${component}`
}
return match
}
return content.replace(regex, replacement)
}
export type FileTree = {
name: string
path?: string
children?: FileTree[]
}
export function createFileTreeForRegistryItemFiles(
files: Array<{ path: string; target?: string }>
) {
const root: FileTree[] = []
for (const file of files) {
const path = file.target ?? file.path
const parts = path.split("/")
let currentLevel = root
for (let i = 0; i < parts.length; i++) {
const part = parts[i]
const isFile = i === parts.length - 1
const existingNode = currentLevel.find((node) => node.name === part)
if (existingNode) {
if (isFile) {
// Update existing file node with full path
existingNode.path = path
} else {
// Move to next level in the tree
currentLevel = existingNode.children!
}
} else {
const newNode: FileTree = isFile
? { name: part, path }
: { name: part, children: [] }
currentLevel.push(newNode)
if (!isFile) {
currentLevel = newNode.children!
}
}
}
}
return root
}