mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-09 06:55:07 +00:00
* chore(apps): Refactor usage of lodash.template to lodash to address security vulnerability * chore(cli): Refactor usage of lodash.template to lodash to address security vulnerability * deps: update lock * chore: changesets * style: fix format * fix: import * chore: build registry --------- Co-authored-by: shadcn <m@shadcn.com>
403 lines
11 KiB
TypeScript
403 lines
11 KiB
TypeScript
import { existsSync, promises as fs } from "fs"
|
|
import path from "path"
|
|
import { DEPRECATED_MESSAGE } from "@/src/deprecated"
|
|
import {
|
|
DEFAULT_COMPONENTS,
|
|
DEFAULT_TAILWIND_CONFIG,
|
|
DEFAULT_TAILWIND_CSS,
|
|
DEFAULT_UTILS,
|
|
getConfig,
|
|
rawConfigSchema,
|
|
resolveConfigPaths,
|
|
type Config,
|
|
} from "@/src/utils/get-config"
|
|
import { getPackageManager } from "@/src/utils/get-package-manager"
|
|
import { getProjectConfig, preFlight } from "@/src/utils/get-project-info"
|
|
import { handleError } from "@/src/utils/handle-error"
|
|
import { logger } from "@/src/utils/logger"
|
|
import {
|
|
getRegistryBaseColor,
|
|
getRegistryBaseColors,
|
|
getRegistryStyles,
|
|
} from "@/src/utils/registry"
|
|
import * as templates from "@/src/utils/templates"
|
|
import chalk from "chalk"
|
|
import { Command } from "commander"
|
|
import { execa } from "execa"
|
|
import template from "lodash/template"
|
|
import ora from "ora"
|
|
import prompts from "prompts"
|
|
import { z } from "zod"
|
|
|
|
import { applyPrefixesCss } from "../utils/transformers/transform-tw-prefix"
|
|
|
|
const PROJECT_DEPENDENCIES = [
|
|
"tailwindcss-animate",
|
|
"class-variance-authority",
|
|
"clsx",
|
|
"tailwind-merge",
|
|
]
|
|
|
|
const initOptionsSchema = z.object({
|
|
cwd: z.string(),
|
|
yes: z.boolean(),
|
|
defaults: z.boolean(),
|
|
})
|
|
|
|
export const init = new Command()
|
|
.name("init")
|
|
.description("initialize your project and install dependencies")
|
|
.option("-y, --yes", "skip confirmation prompt.", false)
|
|
.option("-d, --defaults,", "use default configuration.", false)
|
|
.option(
|
|
"-c, --cwd <cwd>",
|
|
"the working directory. defaults to the current directory.",
|
|
process.cwd()
|
|
)
|
|
.action(async (opts) => {
|
|
try {
|
|
console.log(DEPRECATED_MESSAGE)
|
|
|
|
const options = initOptionsSchema.parse(opts)
|
|
const cwd = path.resolve(options.cwd)
|
|
|
|
// Ensure target directory exists.
|
|
if (!existsSync(cwd)) {
|
|
logger.error(`The path ${cwd} does not exist. Please try again.`)
|
|
process.exit(1)
|
|
}
|
|
|
|
preFlight(cwd)
|
|
|
|
const projectConfig = await getProjectConfig(cwd)
|
|
if (projectConfig) {
|
|
const config = await promptForMinimalConfig(
|
|
cwd,
|
|
projectConfig,
|
|
opts.defaults
|
|
)
|
|
await runInit(cwd, config)
|
|
} else {
|
|
// Read config.
|
|
const existingConfig = await getConfig(cwd)
|
|
const config = await promptForConfig(cwd, existingConfig, options.yes)
|
|
await runInit(cwd, config)
|
|
}
|
|
|
|
logger.info("")
|
|
logger.info(
|
|
`${chalk.green(
|
|
"Success!"
|
|
)} Project initialization completed. You may now add components.`
|
|
)
|
|
logger.info("")
|
|
} catch (error) {
|
|
handleError(error)
|
|
}
|
|
})
|
|
|
|
export async function promptForConfig(
|
|
cwd: string,
|
|
defaultConfig: Config | null = null,
|
|
skip = false
|
|
) {
|
|
const highlight = (text: string) => chalk.cyan(text)
|
|
|
|
const styles = await getRegistryStyles()
|
|
const baseColors = await getRegistryBaseColors()
|
|
|
|
const options = await prompts([
|
|
{
|
|
type: "toggle",
|
|
name: "typescript",
|
|
message: `Would you like to use ${highlight(
|
|
"TypeScript"
|
|
)} (recommended)?`,
|
|
initial: defaultConfig?.tsx ?? true,
|
|
active: "yes",
|
|
inactive: "no",
|
|
},
|
|
{
|
|
type: "select",
|
|
name: "style",
|
|
message: `Which ${highlight("style")} would you like to use?`,
|
|
choices: styles.map((style) => ({
|
|
title: style.label,
|
|
value: style.name,
|
|
})),
|
|
},
|
|
{
|
|
type: "select",
|
|
name: "tailwindBaseColor",
|
|
message: `Which color would you like to use as ${highlight(
|
|
"base color"
|
|
)}?`,
|
|
choices: baseColors.map((color) => ({
|
|
title: color.label,
|
|
value: color.name,
|
|
})),
|
|
},
|
|
{
|
|
type: "text",
|
|
name: "tailwindCss",
|
|
message: `Where is your ${highlight("global CSS")} file?`,
|
|
initial: defaultConfig?.tailwind.css ?? DEFAULT_TAILWIND_CSS,
|
|
},
|
|
{
|
|
type: "toggle",
|
|
name: "tailwindCssVariables",
|
|
message: `Would you like to use ${highlight(
|
|
"CSS variables"
|
|
)} for colors?`,
|
|
initial: defaultConfig?.tailwind.cssVariables ?? true,
|
|
active: "yes",
|
|
inactive: "no",
|
|
},
|
|
{
|
|
type: "text",
|
|
name: "tailwindPrefix",
|
|
message: `Are you using a custom ${highlight(
|
|
"tailwind prefix eg. tw-"
|
|
)}? (Leave blank if not)`,
|
|
initial: "",
|
|
},
|
|
{
|
|
type: "text",
|
|
name: "tailwindConfig",
|
|
message: `Where is your ${highlight("tailwind.config.js")} located?`,
|
|
initial: defaultConfig?.tailwind.config ?? DEFAULT_TAILWIND_CONFIG,
|
|
},
|
|
{
|
|
type: "text",
|
|
name: "components",
|
|
message: `Configure the import alias for ${highlight("components")}:`,
|
|
initial: defaultConfig?.aliases["components"] ?? DEFAULT_COMPONENTS,
|
|
},
|
|
{
|
|
type: "text",
|
|
name: "utils",
|
|
message: `Configure the import alias for ${highlight("utils")}:`,
|
|
initial: defaultConfig?.aliases["utils"] ?? DEFAULT_UTILS,
|
|
},
|
|
{
|
|
type: "toggle",
|
|
name: "rsc",
|
|
message: `Are you using ${highlight("React Server Components")}?`,
|
|
initial: defaultConfig?.rsc ?? true,
|
|
active: "yes",
|
|
inactive: "no",
|
|
},
|
|
])
|
|
|
|
const config = rawConfigSchema.parse({
|
|
$schema: "https://ui.shadcn.com/schema.json",
|
|
style: options.style,
|
|
tailwind: {
|
|
config: options.tailwindConfig,
|
|
css: options.tailwindCss,
|
|
baseColor: options.tailwindBaseColor,
|
|
cssVariables: options.tailwindCssVariables,
|
|
prefix: options.tailwindPrefix,
|
|
},
|
|
rsc: options.rsc,
|
|
tsx: options.typescript,
|
|
aliases: {
|
|
utils: options.utils,
|
|
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
|
|
) {
|
|
const highlight = (text: string) => chalk.cyan(text)
|
|
let style = defaultConfig.style
|
|
let baseColor = defaultConfig.tailwind.baseColor
|
|
let cssVariables = defaultConfig.tailwind.cssVariables
|
|
|
|
if (!defaults) {
|
|
const styles = await getRegistryStyles()
|
|
const baseColors = await getRegistryBaseColors()
|
|
|
|
const options = await prompts([
|
|
{
|
|
type: "select",
|
|
name: "style",
|
|
message: `Which ${highlight("style")} would you like to use?`,
|
|
choices: styles.map((style) => ({
|
|
title: style.label,
|
|
value: style.name,
|
|
})),
|
|
},
|
|
{
|
|
type: "select",
|
|
name: "tailwindBaseColor",
|
|
message: `Which color would you like to use as ${highlight(
|
|
"base color"
|
|
)}?`,
|
|
choices: baseColors.map((color) => ({
|
|
title: color.label,
|
|
value: color.name,
|
|
})),
|
|
},
|
|
{
|
|
type: "toggle",
|
|
name: "tailwindCssVariables",
|
|
message: `Would you like to use ${highlight(
|
|
"CSS variables"
|
|
)} for colors?`,
|
|
initial: defaultConfig?.tailwind.cssVariables,
|
|
active: "yes",
|
|
inactive: "no",
|
|
},
|
|
])
|
|
|
|
style = options.style
|
|
baseColor = options.tailwindBaseColor
|
|
cssVariables = options.tailwindCssVariables
|
|
}
|
|
|
|
const config = rawConfigSchema.parse({
|
|
$schema: defaultConfig?.$schema,
|
|
style,
|
|
tailwind: {
|
|
...defaultConfig?.tailwind,
|
|
baseColor,
|
|
cssVariables,
|
|
},
|
|
rsc: defaultConfig?.rsc,
|
|
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(cwd: string, config: Config) {
|
|
const spinner = ora(`Initializing project...`)?.start()
|
|
|
|
// Ensure all resolved paths directories exist.
|
|
for (const [key, resolvedPath] of Object.entries(config.resolvedPaths)) {
|
|
// Determine if the path is a file or directory.
|
|
// TODO: is there a better way to do this?
|
|
let dirname = path.extname(resolvedPath)
|
|
? path.dirname(resolvedPath)
|
|
: resolvedPath
|
|
|
|
// If the utils alias is set to something like "@/lib/utils",
|
|
// assume this is a file and remove the "utils" file name.
|
|
// TODO: In future releases we should add support for individual utils.
|
|
if (key === "utils" && resolvedPath.endsWith("/utils")) {
|
|
// Remove /utils at the end.
|
|
dirname = dirname.replace(/\/utils$/, "")
|
|
}
|
|
|
|
if (!existsSync(dirname)) {
|
|
await fs.mkdir(dirname, { recursive: true })
|
|
}
|
|
}
|
|
|
|
const extension = config.tsx ? "ts" : "js"
|
|
|
|
const tailwindConfigExtension = path.extname(
|
|
config.resolvedPaths.tailwindConfig
|
|
)
|
|
|
|
let tailwindConfigTemplate: string
|
|
if (tailwindConfigExtension === ".ts") {
|
|
tailwindConfigTemplate = config.tailwind.cssVariables
|
|
? templates.TAILWIND_CONFIG_TS_WITH_VARIABLES
|
|
: templates.TAILWIND_CONFIG_TS
|
|
} else {
|
|
tailwindConfigTemplate = config.tailwind.cssVariables
|
|
? templates.TAILWIND_CONFIG_WITH_VARIABLES
|
|
: templates.TAILWIND_CONFIG
|
|
}
|
|
|
|
// Write tailwind config.
|
|
await fs.writeFile(
|
|
config.resolvedPaths.tailwindConfig,
|
|
template(tailwindConfigTemplate)({
|
|
extension,
|
|
prefix: config.tailwind.prefix,
|
|
}),
|
|
"utf8"
|
|
)
|
|
|
|
// Write css file.
|
|
const baseColor = await getRegistryBaseColor(config.tailwind.baseColor)
|
|
if (baseColor) {
|
|
await fs.writeFile(
|
|
config.resolvedPaths.tailwindCss,
|
|
config.tailwind.cssVariables
|
|
? config.tailwind.prefix
|
|
? applyPrefixesCss(baseColor.cssVarsTemplate, config.tailwind.prefix)
|
|
: baseColor.cssVarsTemplate
|
|
: baseColor.inlineColorsTemplate,
|
|
"utf8"
|
|
)
|
|
}
|
|
|
|
// Write cn file.
|
|
await fs.writeFile(
|
|
`${config.resolvedPaths.utils}.${extension}`,
|
|
extension === "ts" ? templates.UTILS : templates.UTILS_JS,
|
|
"utf8"
|
|
)
|
|
|
|
spinner?.succeed()
|
|
|
|
// Install dependencies.
|
|
const dependenciesSpinner = ora(`Installing dependencies...`)?.start()
|
|
const packageManager = await getPackageManager(cwd)
|
|
|
|
// TODO: add support for other icon libraries.
|
|
const deps = [
|
|
...PROJECT_DEPENDENCIES,
|
|
config.style === "new-york" ? "@radix-ui/react-icons" : "lucide-react",
|
|
]
|
|
|
|
await execa(
|
|
packageManager,
|
|
[packageManager === "npm" ? "install" : "add", ...deps],
|
|
{
|
|
cwd,
|
|
}
|
|
)
|
|
dependenciesSpinner?.succeed()
|
|
}
|