From 2744218d714e229ff7eb260d7c3db16cbb6319f2 Mon Sep 17 00:00:00 2001 From: shadcn Date: Mon, 19 Aug 2024 12:23:49 +0400 Subject: [PATCH] feat(cli): refactor init handling --- .../public/registry/styles/default/index.json | 11 +- .../registry/styles/new-york/index.json | 11 +- apps/www/scripts/build-registry.mts | 30 ++-- packages/cli/package.json | 2 +- packages/cli/src/commands/add.ts | 6 +- packages/cli/src/commands/diff.ts | 14 +- packages/cli/src/commands/init.ts | 153 +++++++++--------- packages/cli/src/utils/handle-error.ts | 20 ++- packages/cli/src/utils/logger.ts | 10 +- packages/cli/src/utils/preflight.ts | 44 ++--- packages/cli/src/utils/registry/index.ts | 45 ++++-- packages/cli/src/utils/registry/schema.ts | 52 +++--- .../{update-utils.ts => update-files.ts} | 6 +- packages/cli/test/commands/init.test.ts | 10 +- pnpm-lock.yaml | 6 +- 15 files changed, 237 insertions(+), 183 deletions(-) rename packages/cli/src/utils/updaters/{update-utils.ts => update-files.ts} (69%) diff --git a/apps/www/public/registry/styles/default/index.json b/apps/www/public/registry/styles/default/index.json index ce444c7ec6..a678637eeb 100644 --- a/apps/www/public/registry/styles/default/index.json +++ b/apps/www/public/registry/styles/default/index.json @@ -25,13 +25,16 @@ ] } }, - "files": [], + "files": [ + { + "name": "utils.ts", + "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}", + "type": "utils" + } + ], "cssVariables": { "light": { "--radius": "0.5rem" - }, - "dark": { - "--radius": "0.5rem" } } } \ No newline at end of file diff --git a/apps/www/public/registry/styles/new-york/index.json b/apps/www/public/registry/styles/new-york/index.json index 8c55e8b538..4f121e4fc3 100644 --- a/apps/www/public/registry/styles/new-york/index.json +++ b/apps/www/public/registry/styles/new-york/index.json @@ -25,13 +25,16 @@ ] } }, - "files": [], + "files": [ + { + "name": "utils.ts", + "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs))\n}", + "type": "utils" + } + ], "cssVariables": { "light": { "--radius": "0.5rem" - }, - "dark": { - "--radius": "0.5rem" } } } \ No newline at end of file diff --git a/apps/www/scripts/build-registry.mts b/apps/www/scripts/build-registry.mts index 5640997c39..6cb03c9990 100644 --- a/apps/www/scripts/build-registry.mts +++ b/apps/www/scripts/build-registry.mts @@ -15,14 +15,6 @@ import { themes } from "../registry/themes" const REGISTRY_PATH = path.join(process.cwd(), "public/registry") -const SHARED_DEPENDENCIES = [ - "tailwindcss-animate", - "class-variance-authority", - "clsx", - "tailwind-merge", - "lucide-react", -] - // ---------------------------------------------------------------------------- // Build __registry__/index.tsx. // ---------------------------------------------------------------------------- @@ -343,7 +335,11 @@ async function buildStylesIndex() { const payload = { name: style.name, dependencies: [ - ...SHARED_DEPENDENCIES, + "tailwindcss-animate", + "class-variance-authority", + "clsx", + "tailwind-merge", + "lucide-react", // TODO: Remove this when we migrate to lucide-react. style.name === "new-york" ? "@radix-ui/react-icons" : "", ], @@ -362,14 +358,22 @@ async function buildStylesIndex() { plugins: [`require("tailwindcss-animate")`], }, }, - files: [], + files: [ + { + name: "utils.ts", + content: `import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +}`, + type: "utils", + }, + ], cssVariables: { light: { "--radius": "0.5rem", }, - dark: { - "--radius": "0.5rem", - }, }, } diff --git a/packages/cli/package.json b/packages/cli/package.json index 87feef406f..c3dd90e692 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -48,7 +48,6 @@ "@babel/core": "^7.22.1", "@babel/parser": "^7.22.6", "@babel/plugin-transform-typescript": "^7.22.5", - "chalk": "5.2.0", "commander": "^10.0.0", "cosmiconfig": "^8.1.3", "deepmerge": "^4.3.1", @@ -57,6 +56,7 @@ "fast-glob": "^3.3.2", "fs-extra": "^11.1.0", "https-proxy-agent": "^6.2.0", + "kleur": "^4.1.5", "lodash.template": "^4.5.0", "node-fetch": "^3.3.0", "ora": "^6.1.2", diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index 14b6401084..1dcdc705eb 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -12,9 +12,9 @@ import { resolveTree, } from "@/src/utils/registry" import { transform } from "@/src/utils/transformers" -import chalk from "chalk" import { Command } from "commander" import { execa } from "execa" +import { green } from "kleur/colors" import ora from "ora" import prompts from "prompts" import { z } from "zod" @@ -58,7 +58,7 @@ export const add = new Command() const config = await getConfig(cwd) if (!config) { logger.error( - `Configuration is missing. Please run ${chalk.green( + `Configuration is missing. Please run ${green( `init` )} to create a components.json file.` ) @@ -149,7 +149,7 @@ export const add = new Command() if (!overwrite) { logger.info( - `Skipped ${item.name}. To overwrite, run with the ${chalk.green( + `Skipped ${item.name}. To overwrite, run with the ${green( "--overwrite" )} flag.` ) diff --git a/packages/cli/src/commands/diff.ts b/packages/cli/src/commands/diff.ts index 43a19f65bd..8ba263c857 100644 --- a/packages/cli/src/commands/diff.ts +++ b/packages/cli/src/commands/diff.ts @@ -11,9 +11,9 @@ import { } from "@/src/utils/registry" import { registryIndexSchema } from "@/src/utils/registry/schema" import { transform } from "@/src/utils/transformers" -import chalk from "chalk" import { Command } from "commander" import { diffLines, type Change } from "diff" +import { green, red } from "kleur/colors" import { z } from "zod" const updateOptionsSchema = z.object({ @@ -50,7 +50,7 @@ export const diff = new Command() const config = await getConfig(cwd) if (!config) { logger.warn( - `Configuration is missing. Please run ${chalk.green( + `Configuration is missing. Please run ${green( `init` )} to create a components.json file.` ) @@ -99,9 +99,7 @@ export const diff = new Command() } } logger.break() - logger.info( - `Run ${chalk.green(`diff `)} to see the changes.` - ) + logger.info(`Run ${green(`diff `)} to see the changes.`) process.exit(0) } @@ -112,7 +110,7 @@ export const diff = new Command() if (!component) { logger.error( - `The component ${chalk.green(options.component)} does not exist.` + `The component ${green(options.component)} does not exist.` ) process.exit(1) } @@ -184,10 +182,10 @@ async function printDiff(diff: Change[]) { diff.forEach((part) => { if (part) { if (part.added) { - return process.stdout.write(chalk.green(part.value)) + return process.stdout.write(green(part.value)) } if (part.removed) { - return process.stdout.write(chalk.red(part.value)) + return process.stdout.write(red(part.value)) } return process.stdout.write(part.value) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index fa38f7f968..ee2028c428 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -17,20 +17,20 @@ import { preFlight } from "@/src/utils/preflight" import { getRegistryBaseColor, getRegistryBaseColors, - getRegistryStyleIndex, + getRegistryItem, getRegistryStyles, } from "@/src/utils/registry" import { updateDependencies } from "@/src/utils/updaters/update-dependencies" import { updateDestinations } from "@/src/utils/updaters/update-destinations" +import { updateFiles } from "@/src/utils/updaters/update-files" import { buildTailwindThemeColorsFromCssVars, updateTailwindConfig, } from "@/src/utils/updaters/update-tailwind-config" import { updateTailwindCss } from "@/src/utils/updaters/update-tailwind-css" -import { updateUtils } from "@/src/utils/updaters/update-utils" -import chalk from "chalk" import { Command } from "commander" import deepmerge from "deepmerge" +import { cyan, green } from "kleur/colors" import ora from "ora" import prompts from "prompts" import { z } from "zod" @@ -39,13 +39,17 @@ const initOptionsSchema = z.object({ cwd: z.string(), yes: z.boolean(), defaults: z.boolean(), + force: z.boolean(), }) +type InitOptions = z.infer + export const init = new Command() .name("init") .description("initialize your project and install dependencies") - .option("-y, --yes", "skip confirmation prompt.", false) + .option("-y, --yes", "skip confirmation prompt.", true) .option("-d, --defaults,", "use default configuration.", false) + .option("-f, --force", "force overwrite of existing configuration.", false) .option( "-c, --cwd ", "the working directory. defaults to the current directory.", @@ -55,7 +59,9 @@ export const init = new Command() try { const options = initOptionsSchema.parse(opts) const cwd = path.resolve(options.cwd) - const { errors } = await preFlight(cwd) + + logger.info("") + const { errors } = await preFlight(cwd, options) if (Object.keys(errors).length > 0) { logger.error( @@ -68,43 +74,63 @@ export const init = new Command() const projectConfig = await getProjectConfig(cwd) const config = projectConfig ? // If we can determine the project config, prompt for minimal config. - await promptForMinimalConfig(cwd, projectConfig, opts.defaults) + await promptForMinimalConfig(projectConfig, options) : // Otherwise, prompt for full config. - await promptForConfig(cwd, await getConfig(cwd), options.yes) + await promptForConfig(await getConfig(cwd)) - await runInit(config) + if (!opts.yes) { + const { proceed } = await prompts({ + type: "confirm", + name: "proceed", + message: `Write configuration to ${cyan( + "components.json" + )}. Proceed?`, + initial: true, + }) + + if (!proceed) { + process.exit(0) + } + } + + // Write to file. + if (!opts.force && !opts.defaults) { + logger.info("") + } + const spinner = ora(`Writing components.json...`).start() + const targetPath = path.resolve(cwd, "components.json") + await fs.writeFile(targetPath, JSON.stringify(config, null, 2), "utf8") + spinner.succeed(`Writing components.json.`) + + const fullConfig = await resolveConfigPaths(cwd, config) + + await runInit(fullConfig) logger.info("") logger.info( - `${chalk.green( + `${green( "Success!" - )} Project initialization completed. You may now add components.` + )} Project initialization completed.\nYou may now add components.` ) logger.info("") } catch (error) { + logger.error("") handleError(error) } }) -export async function promptForConfig( - cwd: string, - defaultConfig: Config | null = null, - skip = false -) { - const highlight = (text: string) => chalk.cyan(text) - +export async function promptForConfig(defaultConfig: Config | null = null) { const [styles, baseColors] = await Promise.all([ getRegistryStyles(), getRegistryBaseColors(), ]) + logger.info("") const options = await prompts([ { type: "toggle", name: "typescript", - message: `Would you like to use ${highlight( - "TypeScript" - )} (recommended)?`, + message: `Would you like to use ${cyan("TypeScript")} (recommended)?`, initial: defaultConfig?.tsx ?? true, active: "yes", inactive: "no", @@ -112,7 +138,7 @@ export async function promptForConfig( { type: "select", name: "style", - message: `Which ${highlight("style")} would you like to use?`, + message: `Which ${cyan("style")} would you like to use?`, choices: styles.map((style) => ({ title: style.label, value: style.name, @@ -121,7 +147,7 @@ export async function promptForConfig( { type: "select", name: "tailwindBaseColor", - message: `Which color would you like to use as the ${highlight( + message: `Which color would you like to use as the ${cyan( "base color" )}?`, choices: baseColors.map((color) => ({ @@ -132,15 +158,13 @@ export async function promptForConfig( { type: "text", name: "tailwindCss", - message: `Where is your ${highlight("global CSS")} file?`, + message: `Where is your ${cyan("global CSS")} file?`, initial: defaultConfig?.tailwind.css ?? DEFAULT_TAILWIND_CSS, }, { type: "toggle", name: "tailwindCssVariables", - message: `Would you like to use ${highlight( - "CSS variables" - )} for theming?`, + message: `Would you like to use ${cyan("CSS variables")} for theming?`, initial: defaultConfig?.tailwind.cssVariables ?? true, active: "yes", inactive: "no", @@ -148,7 +172,7 @@ export async function promptForConfig( { type: "text", name: "tailwindPrefix", - message: `Are you using a custom ${highlight( + message: `Are you using a custom ${cyan( "tailwind prefix eg. tw-" )}? (Leave blank if not)`, initial: "", @@ -156,32 +180,32 @@ export async function promptForConfig( { type: "text", name: "tailwindConfig", - message: `Where is your ${highlight("tailwind.config.js")} located?`, + message: `Where is your ${cyan("tailwind.config.js")} located?`, initial: defaultConfig?.tailwind.config ?? DEFAULT_TAILWIND_CONFIG, }, { type: "text", name: "components", - message: `Configure the import alias for ${highlight("components")}:`, + message: `Configure the import alias for ${cyan("components")}:`, initial: defaultConfig?.aliases["components"] ?? DEFAULT_COMPONENTS, }, { type: "text", name: "utils", - message: `Configure the import alias for ${highlight("utils")}:`, + message: `Configure the import alias for ${cyan("utils")}:`, initial: defaultConfig?.aliases["utils"] ?? DEFAULT_UTILS, }, { type: "toggle", name: "rsc", - message: `Are you using ${highlight("React Server Components")}?`, + message: `Are you using ${cyan("React Server Components")}?`, initial: defaultConfig?.rsc ?? true, active: "yes", inactive: "no", }, ]) - const config = rawConfigSchema.parse({ + return rawConfigSchema.parse({ $schema: "https://ui.shadcn.com/schema.json", style: options.style, tailwind: { @@ -198,53 +222,28 @@ export async function promptForConfig( components: options.components, }, }) - - if (!skip) { - const { proceed } = await prompts({ - type: "confirm", - name: "proceed", - message: `Write configuration to ${highlight( - "components.json" - )}. Proceed?`, - initial: true, - }) - - if (!proceed) { - process.exit(0) - } - } - - // Write to file. - logger.info("") - const spinner = ora(`Writing components.json...`).start() - const targetPath = path.resolve(cwd, "components.json") - await fs.writeFile(targetPath, JSON.stringify(config, null, 2), "utf8") - spinner.succeed() - - return await resolveConfigPaths(cwd, config) } export async function promptForMinimalConfig( - cwd: string, defaultConfig: Config, - defaults = false + opts: InitOptions ) { - const highlight = (text: string) => chalk.cyan(text) let style = defaultConfig.style let baseColor = defaultConfig.tailwind.baseColor let cssVariables = defaultConfig.tailwind.cssVariables - if (!defaults) { + if (!opts.defaults) { const [styles, baseColors] = await Promise.all([ getRegistryStyles(), getRegistryBaseColors(), ]) + logger.info("") const options = await prompts([ { type: "select", name: "style", - message: `Which ${highlight("style")} would you like to use?`, + message: `Which ${cyan("style")} would you like to use?`, choices: styles.map((style) => ({ title: style.label, value: style.name, @@ -254,7 +253,7 @@ export async function promptForMinimalConfig( { type: "select", name: "tailwindBaseColor", - message: `Which color would you like to use as the ${highlight( + message: `Which color would you like to use as the ${cyan( "base color" )}?`, choices: baseColors.map((color) => ({ @@ -265,9 +264,7 @@ export async function promptForMinimalConfig( { type: "toggle", name: "tailwindCssVariables", - message: `Would you like to use ${highlight( - "CSS variables" - )} for theming?`, + message: `Would you like to use ${cyan("CSS variables")} for theming?`, initial: defaultConfig?.tailwind.cssVariables, active: "yes", inactive: "no", @@ -279,7 +276,7 @@ export async function promptForMinimalConfig( cssVariables = options.tailwindCssVariables } - const config = rawConfigSchema.parse({ + return rawConfigSchema.parse({ $schema: defaultConfig?.$schema, style, tailwind: { @@ -291,27 +288,23 @@ export async function promptForMinimalConfig( tsx: defaultConfig?.tsx, aliases: defaultConfig?.aliases, }) - - // Write to file. - logger.info("") - const spinner = ora(`Writing components.json...`).start() - const targetPath = path.resolve(cwd, "components.json") - await fs.writeFile(targetPath, JSON.stringify(config, null, 2), "utf8") - spinner.succeed() - - return await resolveConfigPaths(cwd, config) } export async function runInit(config: Config) { const initializersSpinner = ora(`Initializing project...`)?.start() await updateDestinations(config) const [payload, baseColor] = await Promise.all([ - getRegistryStyleIndex(config.style), + getRegistryItem(config.style, "index"), getRegistryBaseColor(config.tailwind.baseColor), ]) + if (!payload) { + logger.error(`Something went wrong during the initialization process.`) + process.exit(1) + } + // Inline the base color in the tailwind config. - if (config.tailwind.cssVariables) { + if (config.tailwind.cssVariables && baseColor) { payload.cssVars = { light: { ...baseColor.cssVars.light, @@ -345,13 +338,13 @@ export async function runInit(config: Config) { await updateTailwindCss(payload.cssVars, config) } - await updateUtils(config) - initializersSpinner?.succeed() + await updateFiles(payload.files, config) + initializersSpinner?.succeed(`Initializing project.`) // Install dependencies. if (payload.dependencies) { const dependenciesSpinner = ora(`Installing dependencies...`)?.start() await updateDependencies(payload.dependencies, config) - dependenciesSpinner?.succeed() + dependenciesSpinner?.succeed(`Installing dependencies.`) } } diff --git a/packages/cli/src/utils/handle-error.ts b/packages/cli/src/utils/handle-error.ts index 3e3666362f..9f23cb8f73 100644 --- a/packages/cli/src/utils/handle-error.ts +++ b/packages/cli/src/utils/handle-error.ts @@ -1,16 +1,34 @@ import { logger } from "@/src/utils/logger" +import { cyan } from "kleur/colors" +import { z } from "zod" export function handleError(error: unknown) { + logger.error( + `Something went wrong. Please check the error below for more details.` + ) + logger.error(`If the problem persists, please open an issue on GitHub.`) + logger.error("") if (typeof error === "string") { logger.error(error) + logger.error("\n") + process.exit(1) + } + + if (error instanceof z.ZodError) { + logger.error("Validation failed:") + for (const [key, value] of Object.entries(error.flatten().fieldErrors)) { + logger.error(`- ${cyan(key)}: ${value}`) + } + logger.error("\n") process.exit(1) } if (error instanceof Error) { logger.error(error.message) + logger.error("\n") process.exit(1) } - logger.error("Something went wrong. Please try again.") + logger.error("\n") process.exit(1) } diff --git a/packages/cli/src/utils/logger.ts b/packages/cli/src/utils/logger.ts index a18b7b5db8..fb8d6ef2a9 100644 --- a/packages/cli/src/utils/logger.ts +++ b/packages/cli/src/utils/logger.ts @@ -1,17 +1,17 @@ -import chalk from "chalk" +import { cyan, green, red, yellow } from "kleur/colors" export const logger = { error(...args: unknown[]) { - console.log(chalk.red(...args)) + console.log(red(args.join(" "))) }, warn(...args: unknown[]) { - console.log(chalk.yellow(...args)) + console.log(yellow(args.join(" "))) }, info(...args: unknown[]) { - console.log(chalk.cyan(...args)) + console.log(cyan(args.join(" "))) }, success(...args: unknown[]) { - console.log(chalk.green(...args)) + console.log(green(args.join(" "))) }, break() { console.log("") diff --git a/packages/cli/src/utils/preflight.ts b/packages/cli/src/utils/preflight.ts index 640e3481ee..9d0fad8b23 100644 --- a/packages/cli/src/utils/preflight.ts +++ b/packages/cli/src/utils/preflight.ts @@ -2,15 +2,13 @@ import path from "path" import * as ERRORS from "@/src/utils/errors" import { getProjectInfo } from "@/src/utils/get-project-info" import { logger } from "@/src/utils/logger" -import chalk from "chalk" import fs from "fs-extra" +import { cyan } from "kleur/colors" import ora from "ora" -export async function preFlight(cwd: string) { +export async function preFlight(cwd: string, options: { force: boolean }) { const errors: Record = {} - logger.info("") - // Ensure target directory exists. // Check for empty project. We assume if no package.json exists, the project is empty. const projectSpinner = ora(`Preflight checks.`).start() @@ -22,7 +20,7 @@ export async function preFlight(cwd: string) { } // Check for existing components.json file. - if (fs.existsSync(path.resolve(cwd, "components.json"))) { + if (fs.existsSync(path.resolve(cwd, "components.json")) && !options.force) { errors[ERRORS.EXISTING_CONFIG] = true } @@ -31,16 +29,16 @@ export async function preFlight(cwd: string) { logger.info("") if (errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT]) { - logger.error(`The path ${chalk.cyan(cwd)} does not exist or is empty.`) + logger.error(`The path ${cyan(cwd)} does not exist or is empty.`) } if (errors[ERRORS.EXISTING_CONFIG]) { logger.error( - `A ${chalk.cyan("components.json")} file already exists at ${chalk.cyan( + `A ${cyan("components.json")} file already exists at ${cyan( cwd - )}.\nTo start over, remove the ${chalk.cyan( + )}.\nTo start over, remove the ${cyan( "components.json" - )} file and run ${chalk.cyan("init")} again.` + )} file and run ${cyan("init")} again.` ) } @@ -58,8 +56,8 @@ export async function preFlight(cwd: string) { frameworkSpinner?.fail() logger.info("") logger.error( - `We could not detect a supported framework at ${chalk.cyan(cwd)}.\n` + - `Visit ${chalk.cyan( + `We could not detect a supported framework at ${cyan(cwd)}.\n` + + `Visit ${cyan( projectInfo?.framework.links.installation )} to manually configure your project.\nOnce configured, you can use the cli to add components.` ) @@ -92,25 +90,29 @@ export async function preFlight(cwd: string) { if (errors[ERRORS.TAILWIND_NOT_CONFIGURED]) { logger.error( - "Tailwind CSS is not configured. Install Tailwind CSS then run init again.\n" + - `Visit ${chalk.cyan( - projectInfo?.framework.links.tailwind - )} to get started.\n` + `Tailwind CSS is not configured. Install Tailwind CSS then run init again.` ) + if (projectInfo?.framework.links.tailwind) { + logger.info( + `Visit ${cyan(projectInfo?.framework.links.tailwind)} to get started.` + ) + } } if (errors[ERRORS.IMPORT_ALIAS_MISSING]) { - logger.error( - `No import alias found in your tsconfig.json file. \nVisit ${chalk.cyan( - projectInfo?.framework.links.installation - )} to learn how to set an import alias.` - ) + logger.error(`No import alias found in your tsconfig.json file.`) + if (projectInfo?.framework.links.installation) { + logger.info( + `Visit ${cyan( + projectInfo?.framework.links.installation + )} to learn how to set an import alias.` + ) + } } logger.info("") process.exit(1) } - logger.info("") return { errors, diff --git a/packages/cli/src/utils/registry/index.ts b/packages/cli/src/utils/registry/index.ts index ce14a92138..5a8394a6a4 100644 --- a/packages/cli/src/utils/registry/index.ts +++ b/packages/cli/src/utils/registry/index.ts @@ -1,17 +1,23 @@ import path from "path" import { Config } from "@/src/utils/get-config" +import { handleError } from "@/src/utils/handle-error" +import { logger } from "@/src/utils/logger" import { registryBaseColorSchema, registryIndexSchema, + registryItemSchema, registryItemWithContentSchema, registryWithContentSchema, stylesSchema, } from "@/src/utils/registry/schema" import { HttpsProxyAgent } from "https-proxy-agent" +import { cyan } from "kleur/colors" import fetch from "node-fetch" import { z } from "zod" -const baseUrl = process.env.COMPONENTS_REGISTRY_URL ?? "https://ui.shadcn.com" +const REGISTRY_BASE_URL = + process.env.COMPONENTS_REGISTRY_URL ?? "https://ui.shadcn.com" + const agent = process.env.https_proxy ? new HttpsProxyAgent(process.env.https_proxy) : undefined @@ -36,14 +42,15 @@ export async function getRegistryStyles() { } } -export async function getRegistryStyleIndex(style: string) { +export async function getRegistryItem(style: string, name: string) { try { - const [result] = await fetchRegistry([`styles/${style}/index.json`]) + const [result] = await fetchRegistry([`styles/${style}/${name}.json`]) - return registryItemWithContentSchema.parse(result) + return registryItemSchema.parse(result) } catch (error) { - console.log(error) - throw new Error(`Failed to fetch style index from registry.`) + handleError(error) + + return null } } @@ -78,7 +85,7 @@ export async function getRegistryBaseColor(baseColor: string) { return registryBaseColorSchema.parse(result) } catch (error) { - throw new Error(`Failed to fetch base color from registry.`) + handleError(error) } } @@ -151,16 +158,28 @@ async function fetchRegistry(paths: string[]) { try { const results = await Promise.all( paths.map(async (path) => { - const response = await fetch(`${baseUrl}/registry/${path}`, { - agent, - }) - return await response.json() + const url = `${REGISTRY_BASE_URL}/registry/${path}` + const response = await fetch(url, { agent }) + + if (!response.ok) { + const errorMessages: { [key: number]: string } = { + 404: "Not found", + 401: "Unauthorized", + 403: "Forbidden", + 500: "Internal server error", + } + const message = errorMessages[response.status] || response.statusText + throw new Error(`Failed to fetch from ${cyan(url)}. ${message}`) + } + + return response.json() }) ) return results } catch (error) { - console.log(error) - throw new Error(`Failed to fetch registry from ${baseUrl}.`) + logger.error("\n") + handleError(error) + return [] } } diff --git a/packages/cli/src/utils/registry/schema.ts b/packages/cli/src/utils/registry/schema.ts index 06fe78f76d..00c09136e3 100644 --- a/packages/cli/src/utils/registry/schema.ts +++ b/packages/cli/src/utils/registry/schema.ts @@ -1,12 +1,7 @@ import { z } from "zod" -export const registryCssVarsSchema = z.object({ - light: z.record(z.string(), z.string()).optional(), - dark: z.record(z.string(), z.string()).optional(), -}) - // TODO: Extract this to a shared package. -export const registryItemSchema = z.object({ +export const legacyRegistryItemSchema = z.object({ name: z.string(), dependencies: z.array(z.string()).optional(), devDependencies: z.array(z.string()).optional(), @@ -15,21 +10,11 @@ export const registryItemSchema = z.object({ type: z .enum(["components:ui", "components:component", "components:example"]) .optional(), - tailwind: z - .object({ - config: z.object({ - content: z.array(z.string()).optional(), - theme: z.record(z.string(), z.any()).optional(), - plugins: z.array(z.string()).optional(), - }), - }) - .optional(), - cssVars: registryCssVarsSchema.optional(), }) -export const registryIndexSchema = z.array(registryItemSchema) +export const registryIndexSchema = z.array(legacyRegistryItemSchema) -export const registryItemWithContentSchema = registryItemSchema.extend({ +export const registryItemWithContentSchema = legacyRegistryItemSchema.extend({ files: z.array( z.object({ name: z.string(), @@ -64,3 +49,34 @@ export const registryBaseColorSchema = z.object({ inlineColorsTemplate: z.string(), cssVarsTemplate: z.string(), }) + +export const registryCssVarsSchema = z.object({ + light: z.record(z.string(), z.string()).optional(), + dark: z.record(z.string(), z.string()).optional(), +}) + +export const registryItemSchema = z.object({ + name: z.string(), + dependencies: z.array(z.string()).optional(), + devDependencies: z.array(z.string()).optional(), + registryDependencies: z.array(z.string()).optional(), + files: z.array( + z.object({ + name: z.string(), + content: z.string(), + type: z.enum(["utils", "ui", "component"]), + }) + ), + tailwind: z + .object({ + config: z.object({ + content: z.array(z.string()).optional(), + theme: z.record(z.string(), z.any()).optional(), + plugins: z.array(z.string()).optional(), + }), + }) + .optional(), + cssVars: registryCssVarsSchema.optional(), +}) + +export type RegistryItem = z.infer diff --git a/packages/cli/src/utils/updaters/update-utils.ts b/packages/cli/src/utils/updaters/update-files.ts similarity index 69% rename from packages/cli/src/utils/updaters/update-utils.ts rename to packages/cli/src/utils/updaters/update-files.ts index ff037c1cce..318c94f161 100644 --- a/packages/cli/src/utils/updaters/update-utils.ts +++ b/packages/cli/src/utils/updaters/update-files.ts @@ -1,8 +1,12 @@ import { promises as fs } from "fs" import { Config } from "@/src/utils/get-config" +import { RegistryItem } from "@/src/utils/registry/schema" import * as templates from "@/src/utils/templates" -export async function updateUtils(config: Config) { +export async function updateFiles( + files: RegistryItem["files"], + config: Config +) { const extension = config.tsx ? "ts" : "js" await fs.writeFile( `${config.resolvedPaths.utils}.${extension}`, diff --git a/packages/cli/test/commands/init.test.ts b/packages/cli/test/commands/init.test.ts index 7b6e4fb421..f86bb1c9ff 100644 --- a/packages/cli/test/commands/init.test.ts +++ b/packages/cli/test/commands/init.test.ts @@ -25,7 +25,7 @@ test("init config-full", async () => { cssVarsTemplate: "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n", }) - vi.spyOn(registry, "getRegistryStyleIndex").mockResolvedValue({ + vi.spyOn(registry, "getRegistryItem").mockResolvedValue({ name: "new-york", dependencies: [ "tailwindcss-animate", @@ -55,9 +55,6 @@ test("init config-full", async () => { light: { "--radius": "0.5rem", }, - dark: { - "--radius": "0.5rem", - }, }, }) const mockMkdir = vi.spyOn(fs.promises, "mkdir").mockResolvedValue(undefined) @@ -120,7 +117,7 @@ test("init config-partial", async () => { cssVarsTemplate: "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n", }) - vi.spyOn(registry, "getRegistryStyleIndex").mockResolvedValue({ + vi.spyOn(registry, "getRegistryItem").mockResolvedValue({ name: "new-york", dependencies: [ "tailwindcss-animate", @@ -149,9 +146,6 @@ test("init config-partial", async () => { light: { "--radius": "0.5rem", }, - dark: { - "--radius": "0.5rem", - }, }, }) const mockMkdir = vi.spyOn(fs.promises, "mkdir").mockResolvedValue(undefined) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 335f2af23f..885bfdd098 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -388,9 +388,6 @@ importers: '@babel/plugin-transform-typescript': specifier: ^7.22.5 version: 7.22.5(@babel/core@7.22.1) - chalk: - specifier: 5.2.0 - version: 5.2.0 commander: specifier: ^10.0.0 version: 10.0.0 @@ -415,6 +412,9 @@ importers: https-proxy-agent: specifier: ^6.2.0 version: 6.2.0 + kleur: + specifier: ^4.1.5 + version: 4.1.5 lodash.template: specifier: ^4.5.0 version: 4.5.0