Skip to main content
Glama
reuvenaor

Shadcn Registry manager

by reuvenaor
get-config.ts6.54 kB
import path from "path" import { getProjectInfo } from "@/src/utils/get-project-info" import { resolveImport } from "@/src/utils/resolve-import" import { cosmiconfig } from "cosmiconfig" import fg from "fast-glob" import { loadConfig } from "tsconfig-paths" import { z } from "zod" export const DEFAULT_STYLE = "default" export const DEFAULT_COMPONENTS = "@/components" export const DEFAULT_UTILS = "@/lib/utils" export const DEFAULT_TAILWIND_CSS = "app/globals.css" export const DEFAULT_TAILWIND_CONFIG = "tailwind.config.js" export const DEFAULT_TAILWIND_BASE_COLOR = "slate" // TODO: Figure out if we want to support all cosmiconfig formats. // A simple components.json file would be nice. const explorer = cosmiconfig("components", { searchPlaces: ["components.json"], }) export const rawConfigSchema = z .object({ $schema: z.string().optional(), style: z.string(), rsc: z.coerce.boolean().default(false), tsx: z.coerce.boolean().default(true), tailwind: z.object({ config: z.string().optional(), css: z.string(), baseColor: z.string(), cssVariables: z.boolean().default(true), prefix: z.string().default("").optional(), }), aliases: z.object({ components: z.string(), utils: z.string(), ui: z.string().optional(), lib: z.string().optional(), hooks: z.string().optional(), }), iconLibrary: z.string().optional(), }) .strict() export type RawConfig = z.infer<typeof rawConfigSchema> export const configSchema = rawConfigSchema.extend({ resolvedPaths: z.object({ cwd: z.string(), tailwindConfig: z.string(), tailwindCss: z.string(), utils: z.string(), components: z.string(), lib: z.string(), hooks: z.string(), ui: z.string(), }), }) export type Config = z.infer<typeof configSchema> // TODO: type the key. // Okay for now since I don't want a breaking change. export const workspaceConfigSchema = z.record(configSchema) export async function getConfig(cwd: string) { const config = await getRawConfig(cwd) if (!config) { 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) } export async function resolveConfigPaths(cwd: string, config: RawConfig) { // Read tsconfig.json. const tsConfig = await loadConfig(cwd) if (tsConfig.resultType === "failed") { throw new Error( `Failed to load ${config.tsx ? "tsconfig" : "jsconfig"}.json. ${tsConfig.message ?? "" }`.trim() ) } return configSchema.parse({ ...config, resolvedPaths: { cwd, tailwindConfig: config.tailwind.config ? path.resolve(cwd, config.tailwind.config) : "", tailwindCss: path.resolve(cwd, config.tailwind.css), utils: await resolveImport(config.aliases["utils"], tsConfig), components: await resolveImport(config.aliases["components"], tsConfig), ui: config.aliases["ui"] ? await resolveImport(config.aliases["ui"], tsConfig) : path.resolve( (await resolveImport(config.aliases["components"], tsConfig)) ?? cwd, "ui" ), // TODO: Make this configurable. // For now, we assume the lib and hooks directories are one level up from the components directory. lib: config.aliases["lib"] ? await resolveImport(config.aliases["lib"], tsConfig) : path.resolve( (await resolveImport(config.aliases["utils"], tsConfig)) ?? cwd, ".." ), hooks: config.aliases["hooks"] ? await resolveImport(config.aliases["hooks"], tsConfig) : path.resolve( (await resolveImport(config.aliases["components"], tsConfig)) ?? cwd, "..", "hooks" ), }, }) } export async function getRawConfig(cwd: string): Promise<RawConfig | null> { try { const configResult = await explorer.search(cwd) if (!configResult) { return null } return rawConfigSchema.parse(configResult.config) } catch (error) { const componentPath = `${cwd}/components.json` throw new Error( `Invalid configuration found in ${componentPath}.` ) } } // Note: we can check for -workspace.yaml or "workspace" in package.json. // Since cwd is not necessarily the root of the project. // We'll instead check if ui aliases resolve to a different root. export async function getWorkspaceConfig(config: Config) { let resolvedAliases: any = {} for (const key of Object.keys(config.aliases)) { if (!isAliasKey(key, config)) { continue } const resolvedPath = config.resolvedPaths[key] const packageRoot = await findPackageRoot( config.resolvedPaths.cwd, resolvedPath ) if (!packageRoot) { resolvedAliases[key] = config continue } resolvedAliases[key] = await getConfig(packageRoot) } const result = workspaceConfigSchema.safeParse(resolvedAliases) if (!result.success) { return null } return result.data } export async function findPackageRoot(cwd: string, resolvedPath: string) { const commonRoot = findCommonRoot(cwd, resolvedPath) const relativePath = path.relative(commonRoot, resolvedPath) const packageRoots = await fg.glob("**/package.json", { cwd: commonRoot, deep: 3, ignore: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/public/**"], }) const matchingPackageRoot = packageRoots .map((pkgPath) => path.dirname(pkgPath)) .find((pkgDir) => relativePath.startsWith(pkgDir)) return matchingPackageRoot ? path.join(commonRoot, matchingPackageRoot) : null } function isAliasKey( key: string, config: Config ): key is keyof Config["aliases"] { return Object.keys(config.resolvedPaths) .filter((key) => key !== "utils") .includes(key) } export function findCommonRoot(cwd: string, resolvedPath: string) { const parts1 = cwd.split(path.sep) const parts2 = resolvedPath.split(path.sep) const commonParts = [] for (let i = 0; i < Math.min(parts1.length, parts2.length); i++) { if (parts1[i] !== parts2[i]) { break } commonParts.push(parts1[i]) } return commonParts.join(path.sep) } // TODO: Cache this call. export async function getTargetStyleFromConfig(cwd: string, fallback: string) { const projectInfo = await getProjectInfo(cwd) return projectInfo?.tailwindVersion === "v4" ? "new-york-v4" : fallback }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/reuvenaor/shadcn-registry-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server