mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-26 22:26:05 +00:00
feat(cli): refactor init handling
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.`
|
||||
)
|
||||
|
||||
@@ -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 <component>`)} to see the changes.`
|
||||
)
|
||||
logger.info(`Run ${green(`diff <component>`)} 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)
|
||||
|
||||
@@ -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<typeof initOptionsSchema>
|
||||
|
||||
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 <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.`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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("")
|
||||
|
||||
@@ -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<string, boolean> = {}
|
||||
|
||||
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,
|
||||
|
||||
@@ -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 []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<typeof registryItemSchema>
|
||||
|
||||
@@ -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}`,
|
||||
@@ -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)
|
||||
|
||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user