feat(cli): refactor init handling

This commit is contained in:
shadcn
2024-08-19 12:23:49 +04:00
parent b95ffc2168
commit 2744218d71
15 changed files with 237 additions and 183 deletions

View File

@@ -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"
}
}
}

View File

@@ -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"
}
}
}

View File

@@ -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",
},
},
}

View File

@@ -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",

View File

@@ -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.`
)

View File

@@ -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)

View File

@@ -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.`)
}
}

View File

@@ -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)
}

View File

@@ -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("")

View File

@@ -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,

View File

@@ -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 []
}
}

View File

@@ -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>

View File

@@ -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}`,

View File

@@ -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
View File

@@ -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