Files
shadcn-ui/packages/cli/src/commands/init.ts
Jens Astrup a1bed464f3 chore(apps): Update lodash (#4397)
* 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>
2024-12-09 12:33:28 +04:00

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