mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-08 14:35:09 +00:00
feat(cli): update framework info handling
This commit is contained in:
@@ -57,11 +57,12 @@ export const add = new Command()
|
||||
|
||||
const config = await getConfig(cwd)
|
||||
if (!config) {
|
||||
logger.warn(
|
||||
logger.error(
|
||||
`Configuration is missing. Please run ${chalk.green(
|
||||
`init`
|
||||
)} to create a components.json file.`
|
||||
)
|
||||
logger.error("")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { existsSync, promises as fs } from "fs"
|
||||
import path from "path"
|
||||
import * as ERRORS from "@/src/utils/errors"
|
||||
import {
|
||||
DEFAULT_COMPONENTS,
|
||||
DEFAULT_TAILWIND_CONFIG,
|
||||
@@ -11,9 +12,10 @@ import {
|
||||
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 { getProjectConfig } from "@/src/utils/get-project-info"
|
||||
import { handleError } from "@/src/utils/handle-error"
|
||||
import { logger } from "@/src/utils/logger"
|
||||
import { preFlight } from "@/src/utils/preflight"
|
||||
import {
|
||||
getRegistryBaseColor,
|
||||
getRegistryBaseColors,
|
||||
@@ -57,29 +59,65 @@ export const init = new Command()
|
||||
try {
|
||||
const options = initOptionsSchema.parse(opts)
|
||||
const cwd = path.resolve(options.cwd)
|
||||
const preflightResult = await preFlight(cwd)
|
||||
|
||||
// Ensure target directory exists.
|
||||
if (!existsSync(cwd)) {
|
||||
logger.error(`The path ${cwd} does not exist. Please try again.`)
|
||||
if (preflightResult.error === ERRORS.MISSING_DIR) {
|
||||
logger.error(
|
||||
`The path ${cwd} does not exist. Make sure it exists and try again.`
|
||||
)
|
||||
logger.error("")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
preFlight(cwd)
|
||||
if (preflightResult.error === ERRORS.EXISTING_CONFIG) {
|
||||
logger.error(`The path ${cwd} already contains a components.json file.`)
|
||||
logger.error(
|
||||
"To start over, remove the components.json file and try again."
|
||||
)
|
||||
logger.error("")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (preflightResult.error === ERRORS.TAILWIND_NOT_CONFIGURED) {
|
||||
const framework =
|
||||
preflightResult.info?.framework &&
|
||||
["next-app", "next-pages"].includes(preflightResult.info?.framework)
|
||||
? "nextjs"
|
||||
: preflightResult.info?.framework
|
||||
const tailwindInstallationUrl = framework
|
||||
? `https://tailwindcss.com/docs/guides/${framework}`
|
||||
: "https://tailwindcss.com/docs/installation/framework-guides"
|
||||
|
||||
logger.error(
|
||||
"Tailwind CSS is not configured. Install Tailwind CSS then run init again.\n" +
|
||||
`Visit ${tailwindInstallationUrl} to get started.\n`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (preflightResult.error === ERRORS.IMPORT_ALIAS_MISSING) {
|
||||
const framework =
|
||||
preflightResult.info?.framework &&
|
||||
["next-app", "next-pages"].includes(preflightResult.info?.framework)
|
||||
? "next"
|
||||
: preflightResult.info?.framework
|
||||
logger.error(
|
||||
`No import alias found in your tsconfig.json file. \nVisit ${chalk.cyan(
|
||||
`https://ui.shadcn.com/docs/installation/${framework}`
|
||||
)} to learn how to set an import alias.`
|
||||
)
|
||||
logger.error("")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
const config = projectConfig
|
||||
? // If we can determine the project config, prompt for minimal config.
|
||||
await promptForMinimalConfig(cwd, projectConfig, opts.defaults)
|
||||
: // Otherwise, prompt for full config.
|
||||
await promptForConfig(cwd, await getConfig(cwd), options.yes)
|
||||
|
||||
await runInit(cwd, config)
|
||||
|
||||
logger.info("")
|
||||
logger.info(
|
||||
@@ -100,8 +138,10 @@ export async function promptForConfig(
|
||||
) {
|
||||
const highlight = (text: string) => chalk.cyan(text)
|
||||
|
||||
const styles = await getRegistryStyles()
|
||||
const baseColors = await getRegistryBaseColors()
|
||||
const [styles, baseColors] = await Promise.all([
|
||||
getRegistryStyles(),
|
||||
getRegistryBaseColors(),
|
||||
])
|
||||
|
||||
const options = await prompts([
|
||||
{
|
||||
@@ -240,8 +280,10 @@ export async function promptForMinimalConfig(
|
||||
let cssVariables = defaultConfig.tailwind.cssVariables
|
||||
|
||||
if (!defaults) {
|
||||
const styles = await getRegistryStyles()
|
||||
const baseColors = await getRegistryBaseColors()
|
||||
const [styles, baseColors] = await Promise.all([
|
||||
getRegistryStyles(),
|
||||
getRegistryBaseColors(),
|
||||
])
|
||||
|
||||
const options = await prompts([
|
||||
{
|
||||
@@ -252,6 +294,7 @@ export async function promptForMinimalConfig(
|
||||
title: style.label,
|
||||
value: style.name,
|
||||
})),
|
||||
initial: styles.findIndex((s) => s.name === style),
|
||||
},
|
||||
{
|
||||
type: "select",
|
||||
|
||||
5
packages/cli/src/utils/errors.ts
Normal file
5
packages/cli/src/utils/errors.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const MISSING_DIR = "0"
|
||||
export const EXISTING_CONFIG = "1"
|
||||
export const EMPTY_PROJECT = "2"
|
||||
export const TAILWIND_NOT_CONFIGURED = "3"
|
||||
export const IMPORT_ALIAS_MISSING = "4"
|
||||
@@ -1,4 +1,3 @@
|
||||
import { existsSync } from "fs"
|
||||
import path from "path"
|
||||
import {
|
||||
Config,
|
||||
@@ -10,20 +9,20 @@ import fg from "fast-glob"
|
||||
import fs, { pathExists } from "fs-extra"
|
||||
import { loadConfig } from "tsconfig-paths"
|
||||
|
||||
// TODO: Add support for more frameworks.
|
||||
// We'll start with Next.js for now.
|
||||
const PROJECT_TYPES = [
|
||||
const SUPPORTED_FRAMEWORKS = [
|
||||
"next-app",
|
||||
"next-pages",
|
||||
"remix",
|
||||
"astro",
|
||||
"vite",
|
||||
] as const
|
||||
|
||||
type ProjectType = {
|
||||
framework: (typeof PROJECT_TYPES)[number]
|
||||
type ProjectInfo = {
|
||||
framework: (typeof SUPPORTED_FRAMEWORKS)[number]
|
||||
isUsingSrcDir: boolean
|
||||
isTypescript: boolean
|
||||
tailwindConfigFile: string | null
|
||||
tailwindCssFile: string | null
|
||||
tsConfigAliasPrefix: string | null
|
||||
}
|
||||
|
||||
const PROJECT_SHARED_IGNORE = [
|
||||
@@ -34,61 +33,15 @@ const PROJECT_SHARED_IGNORE = [
|
||||
"build",
|
||||
]
|
||||
|
||||
export async function getTsConfig() {
|
||||
try {
|
||||
const tsconfigPath = path.join("tsconfig.json")
|
||||
const tsconfig = await fs.readJSON(tsconfigPath)
|
||||
|
||||
if (!tsconfig) {
|
||||
throw new Error("tsconfig.json is missing")
|
||||
}
|
||||
|
||||
return tsconfig
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProjectConfig(cwd: string): Promise<Config | null> {
|
||||
// Check for existing component config.
|
||||
const existingConfig = await getConfig(cwd)
|
||||
if (existingConfig) {
|
||||
return existingConfig
|
||||
}
|
||||
|
||||
const projectType = await getProjectType(cwd)
|
||||
const tailwindCssFile = await getTailwindCssFile(cwd)
|
||||
const tsConfigAliasPrefix = await getTsConfigAliasPrefix(cwd)
|
||||
|
||||
if (!projectType || !tailwindCssFile || !tsConfigAliasPrefix) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isTsx = await isTypeScriptProject(cwd)
|
||||
|
||||
const config: RawConfig = {
|
||||
$schema: "https://ui.shadcn.com/schema.json",
|
||||
rsc: ["next-app", "next-app-src"].includes(projectType.framework),
|
||||
tsx: isTsx,
|
||||
style: "new-york",
|
||||
tailwind: {
|
||||
config: isTsx ? "tailwind.config.ts" : "tailwind.config.js",
|
||||
baseColor: "zinc",
|
||||
css: tailwindCssFile,
|
||||
cssVariables: true,
|
||||
prefix: "",
|
||||
},
|
||||
aliases: {
|
||||
utils: `${tsConfigAliasPrefix}/lib/utils`,
|
||||
components: `${tsConfigAliasPrefix}/components`,
|
||||
},
|
||||
}
|
||||
|
||||
return await resolveConfigPaths(cwd, config)
|
||||
}
|
||||
|
||||
export async function getProjectType(cwd: string): Promise<ProjectType | null> {
|
||||
const [configFiles, isUsingSrcDir, isTypescript] = await Promise.all([
|
||||
export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
||||
const [
|
||||
configFiles,
|
||||
isUsingSrcDir,
|
||||
isTypescript,
|
||||
tailwindConfigFile,
|
||||
tailwindCssFile,
|
||||
tsConfigAliasPrefix,
|
||||
] = await Promise.all([
|
||||
fg.glob("**/{next,vite,astro}.config.*", {
|
||||
cwd,
|
||||
deep: 3,
|
||||
@@ -96,7 +49,11 @@ export async function getProjectType(cwd: string): Promise<ProjectType | null> {
|
||||
}),
|
||||
fs.pathExists(path.resolve(cwd, "src")),
|
||||
isTypeScriptProject(cwd),
|
||||
getTailwindConfigFile(cwd),
|
||||
getTailwindCssFile(cwd),
|
||||
getTsConfigAliasPrefix(cwd),
|
||||
])
|
||||
|
||||
const isUsingAppDir = await fs.pathExists(
|
||||
path.resolve(cwd, `${isUsingSrcDir ? "src/" : ""}app`)
|
||||
)
|
||||
@@ -105,10 +62,13 @@ export async function getProjectType(cwd: string): Promise<ProjectType | null> {
|
||||
return null
|
||||
}
|
||||
|
||||
const type: ProjectType = {
|
||||
const type: ProjectInfo = {
|
||||
framework: "next-app",
|
||||
isUsingSrcDir,
|
||||
isTypescript,
|
||||
tailwindConfigFile,
|
||||
tailwindCssFile,
|
||||
tsConfigAliasPrefix,
|
||||
}
|
||||
|
||||
// Next.js.
|
||||
@@ -117,13 +77,6 @@ export async function getProjectType(cwd: string): Promise<ProjectType | null> {
|
||||
return type
|
||||
}
|
||||
|
||||
// Astro.
|
||||
if (configFiles.find((file) => file.startsWith("astro.config."))?.length) {
|
||||
type.framework = "astro"
|
||||
type.isTypescript = true
|
||||
return type
|
||||
}
|
||||
|
||||
// Vite and Remix.
|
||||
// They both have a vite.config.* file.
|
||||
if (configFiles.find((file) => file.startsWith("vite.config."))?.length) {
|
||||
@@ -159,6 +112,20 @@ export async function getTailwindCssFile(cwd: string) {
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getTailwindConfigFile(cwd: string) {
|
||||
const files = await fg.glob("tailwind.config.*", {
|
||||
cwd,
|
||||
deep: 3,
|
||||
ignore: PROJECT_SHARED_IGNORE,
|
||||
})
|
||||
|
||||
if (!files.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return files[0]
|
||||
}
|
||||
|
||||
export async function getTsConfigAliasPrefix(cwd: string) {
|
||||
const tsConfig = await loadConfig(cwd)
|
||||
|
||||
@@ -168,8 +135,12 @@ export async function getTsConfigAliasPrefix(cwd: string) {
|
||||
|
||||
// This assume that the first alias is the prefix.
|
||||
for (const [alias, paths] of Object.entries(tsConfig.paths)) {
|
||||
if (paths.includes("./*") || paths.includes("./src/*")) {
|
||||
return alias.at(0)
|
||||
if (
|
||||
paths.includes("./*") ||
|
||||
paths.includes("./src/*") ||
|
||||
paths.includes("./app/*")
|
||||
) {
|
||||
return alias.at(0) ?? null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,23 +148,66 @@ export async function getTsConfigAliasPrefix(cwd: string) {
|
||||
}
|
||||
|
||||
export async function isTypeScriptProject(cwd: string) {
|
||||
// Check if cwd has a tsconfig.json file.
|
||||
return pathExists(path.resolve(cwd, "tsconfig.json"))
|
||||
}
|
||||
|
||||
export async function preFlight(cwd: string) {
|
||||
// We need Tailwind CSS to be configured.
|
||||
const tailwindConfig = await fg.glob("tailwind.config.*", {
|
||||
const files = await fg.glob("tsconfig.*", {
|
||||
cwd,
|
||||
deep: 3,
|
||||
deep: 1,
|
||||
ignore: PROJECT_SHARED_IGNORE,
|
||||
})
|
||||
|
||||
if (!tailwindConfig.length) {
|
||||
throw new Error(
|
||||
"Tailwind CSS is not installed. Visit https://tailwindcss.com/docs/installation to get started."
|
||||
)
|
||||
return files.length > 0
|
||||
}
|
||||
|
||||
export async function getTsConfig() {
|
||||
try {
|
||||
const tsconfigPath = path.join("tsconfig.json")
|
||||
const tsconfig = await fs.readJSON(tsconfigPath)
|
||||
|
||||
if (!tsconfig) {
|
||||
throw new Error("tsconfig.json is missing")
|
||||
}
|
||||
|
||||
return tsconfig
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProjectConfig(cwd: string): Promise<Config | null> {
|
||||
// Check for existing component config.
|
||||
const [existingConfig, projectInfo] = await Promise.all([
|
||||
getConfig(cwd),
|
||||
getProjectInfo(cwd),
|
||||
])
|
||||
|
||||
if (existingConfig) {
|
||||
return existingConfig
|
||||
}
|
||||
|
||||
return true
|
||||
if (
|
||||
!projectInfo ||
|
||||
!projectInfo.tailwindConfigFile ||
|
||||
!projectInfo.tailwindCssFile
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const config: RawConfig = {
|
||||
$schema: "https://ui.shadcn.com/schema.json",
|
||||
rsc: ["next-app", "next-app-src"].includes(projectInfo.framework),
|
||||
tsx: projectInfo.isTypescript,
|
||||
style: "new-york",
|
||||
tailwind: {
|
||||
config: projectInfo.tailwindConfigFile,
|
||||
baseColor: "zinc",
|
||||
css: projectInfo.tailwindCssFile,
|
||||
cssVariables: true,
|
||||
prefix: "",
|
||||
},
|
||||
aliases: {
|
||||
utils: `${projectInfo.tsConfigAliasPrefix}/lib/utils`,
|
||||
components: `${projectInfo.tsConfigAliasPrefix}/components`,
|
||||
},
|
||||
}
|
||||
|
||||
return await resolveConfigPaths(cwd, config)
|
||||
}
|
||||
|
||||
51
packages/cli/src/utils/preflight.ts
Normal file
51
packages/cli/src/utils/preflight.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import path from "path"
|
||||
import * as ERRORS from "@/src/utils/errors"
|
||||
import { getProjectInfo } from "@/src/utils/get-project-info"
|
||||
import fs from "fs-extra"
|
||||
|
||||
export async function preFlight(cwd: string) {
|
||||
// Ensure target directory exists.
|
||||
if (!fs.existsSync(cwd)) {
|
||||
return {
|
||||
error: ERRORS.MISSING_DIR,
|
||||
info: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Check for existing components.json file.
|
||||
if (fs.existsSync(path.resolve(cwd, "components.json"))) {
|
||||
return {
|
||||
error: ERRORS.EXISTING_CONFIG,
|
||||
info: null,
|
||||
}
|
||||
}
|
||||
|
||||
// Check for empty project. We assume if no package.json exists, the project is empty.
|
||||
if (!fs.existsSync(path.resolve(cwd, "package.json"))) {
|
||||
return {
|
||||
error: ERRORS.EMPTY_PROJECT,
|
||||
info: null,
|
||||
}
|
||||
}
|
||||
|
||||
const projectInfo = await getProjectInfo(cwd)
|
||||
|
||||
if (!projectInfo?.tailwindConfigFile || !projectInfo?.tailwindCssFile) {
|
||||
return {
|
||||
error: ERRORS.TAILWIND_NOT_CONFIGURED,
|
||||
info: projectInfo,
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectInfo.tsConfigAliasPrefix) {
|
||||
return {
|
||||
error: ERRORS.IMPORT_ALIAS_MISSING,
|
||||
info: projectInfo,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error: null,
|
||||
info: projectInfo,
|
||||
}
|
||||
}
|
||||
@@ -39,8 +39,8 @@ export async function getRegistryStyles() {
|
||||
export async function getRegistryBaseColors() {
|
||||
return [
|
||||
{
|
||||
name: "slate",
|
||||
label: "Slate",
|
||||
name: "neutral",
|
||||
label: "Neutral",
|
||||
},
|
||||
{
|
||||
name: "gray",
|
||||
@@ -50,14 +50,14 @@ export async function getRegistryBaseColors() {
|
||||
name: "zinc",
|
||||
label: "Zinc",
|
||||
},
|
||||
{
|
||||
name: "neutral",
|
||||
label: "Neutral",
|
||||
},
|
||||
{
|
||||
name: "stone",
|
||||
label: "Stone",
|
||||
},
|
||||
{
|
||||
name: "slate",
|
||||
label: "Slate",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user