mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
feat(cli): update preflight handling
This commit is contained in:
@@ -59,52 +59,11 @@ export const init = new Command()
|
||||
try {
|
||||
const options = initOptionsSchema.parse(opts)
|
||||
const cwd = path.resolve(options.cwd)
|
||||
const preflightResult = await preFlight(cwd)
|
||||
const { errors, projectInfo } = await preFlight(cwd)
|
||||
|
||||
if (preflightResult.error === ERRORS.MISSING_DIR) {
|
||||
if (Object.keys(errors).length > 0) {
|
||||
logger.error(
|
||||
`The path ${cwd} does not exist. Make sure it exists and try again.`
|
||||
)
|
||||
logger.error("")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
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.`
|
||||
`Something went wrong during the preflight check. Please check the output above for more details.`
|
||||
)
|
||||
logger.error("")
|
||||
process.exit(1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const MISSING_DIR = "0"
|
||||
export const EXISTING_CONFIG = "1"
|
||||
export const EMPTY_PROJECT = "2"
|
||||
export const MISSING_DIR_OR_EMPTY_PROJECT = "1"
|
||||
export const EXISTING_CONFIG = "2"
|
||||
export const TAILWIND_NOT_CONFIGURED = "3"
|
||||
export const IMPORT_ALIAS_MISSING = "4"
|
||||
export const UNSUPPORTED_FRAMEWORK = "5"
|
||||
|
||||
@@ -18,11 +18,12 @@ const SUPPORTED_FRAMEWORKS = [
|
||||
|
||||
type ProjectInfo = {
|
||||
framework: (typeof SUPPORTED_FRAMEWORKS)[number]
|
||||
isUsingSrcDir: boolean
|
||||
isTypescript: boolean
|
||||
isSrcDir: boolean
|
||||
isRSC: boolean
|
||||
isTsx: boolean
|
||||
tailwindConfigFile: string | null
|
||||
tailwindCssFile: string | null
|
||||
tsConfigAliasPrefix: string | null
|
||||
aliasPrefix: string | null
|
||||
}
|
||||
|
||||
const PROJECT_SHARED_IGNORE = [
|
||||
@@ -36,11 +37,11 @@ const PROJECT_SHARED_IGNORE = [
|
||||
export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
||||
const [
|
||||
configFiles,
|
||||
isUsingSrcDir,
|
||||
isTypescript,
|
||||
isSrcDir,
|
||||
isTsx,
|
||||
tailwindConfigFile,
|
||||
tailwindCssFile,
|
||||
tsConfigAliasPrefix,
|
||||
aliasPrefix,
|
||||
] = await Promise.all([
|
||||
fg.glob("**/{next,vite,astro}.config.*", {
|
||||
cwd,
|
||||
@@ -55,7 +56,7 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
||||
])
|
||||
|
||||
const isUsingAppDir = await fs.pathExists(
|
||||
path.resolve(cwd, `${isUsingSrcDir ? "src/" : ""}app`)
|
||||
path.resolve(cwd, `${isSrcDir ? "src/" : ""}app`)
|
||||
)
|
||||
|
||||
if (!configFiles.length) {
|
||||
@@ -64,16 +65,18 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
||||
|
||||
const type: ProjectInfo = {
|
||||
framework: "next-app",
|
||||
isUsingSrcDir,
|
||||
isTypescript,
|
||||
isSrcDir,
|
||||
isRSC: false,
|
||||
isTsx,
|
||||
tailwindConfigFile,
|
||||
tailwindCssFile,
|
||||
tsConfigAliasPrefix,
|
||||
aliasPrefix,
|
||||
}
|
||||
|
||||
// Next.js.
|
||||
if (configFiles.find((file) => file.startsWith("next.config."))?.length) {
|
||||
type.framework = isUsingAppDir ? "next-app" : "next-pages"
|
||||
type.isRSC = isUsingAppDir
|
||||
return type
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ export async function getProjectInfo(cwd: string): Promise<ProjectInfo | null> {
|
||||
export async function getTailwindCssFile(cwd: string) {
|
||||
const files = await fg.glob("**/*.css", {
|
||||
cwd,
|
||||
deep: 3,
|
||||
deep: 5,
|
||||
ignore: PROJECT_SHARED_IGNORE,
|
||||
})
|
||||
|
||||
@@ -193,8 +196,8 @@ export async function getProjectConfig(cwd: string): Promise<Config | null> {
|
||||
|
||||
const config: RawConfig = {
|
||||
$schema: "https://ui.shadcn.com/schema.json",
|
||||
rsc: ["next-app", "next-app-src"].includes(projectInfo.framework),
|
||||
tsx: projectInfo.isTypescript,
|
||||
rsc: projectInfo.isRSC,
|
||||
tsx: projectInfo.isTsx,
|
||||
style: "new-york",
|
||||
tailwind: {
|
||||
config: projectInfo.tailwindConfigFile,
|
||||
@@ -204,8 +207,8 @@ export async function getProjectConfig(cwd: string): Promise<Config | null> {
|
||||
prefix: "",
|
||||
},
|
||||
aliases: {
|
||||
utils: `${projectInfo.tsConfigAliasPrefix}/lib/utils`,
|
||||
components: `${projectInfo.tsConfigAliasPrefix}/components`,
|
||||
utils: `${projectInfo.aliasPrefix}/lib/utils`,
|
||||
components: `${projectInfo.aliasPrefix}/components`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,124 @@
|
||||
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 ora from "ora"
|
||||
|
||||
export async function preFlight(cwd: string) {
|
||||
const errors: Record<string, boolean> = {}
|
||||
|
||||
// Ensure target directory exists.
|
||||
if (!fs.existsSync(cwd)) {
|
||||
return {
|
||||
error: ERRORS.MISSING_DIR,
|
||||
info: null,
|
||||
}
|
||||
// Check for empty project. We assume if no package.json exists, the project is empty.
|
||||
const projectSpinner = ora(`Running preflight checks...`).start()
|
||||
if (
|
||||
!fs.existsSync(cwd) ||
|
||||
!fs.existsSync(path.resolve(cwd, "package.json"))
|
||||
) {
|
||||
errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT] = true
|
||||
}
|
||||
|
||||
// Check for existing components.json file.
|
||||
if (fs.existsSync(path.resolve(cwd, "components.json"))) {
|
||||
return {
|
||||
error: ERRORS.EXISTING_CONFIG,
|
||||
info: null,
|
||||
}
|
||||
errors[ERRORS.EXISTING_CONFIG] = true
|
||||
}
|
||||
|
||||
// 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,
|
||||
if (Object.keys(errors).length > 0) {
|
||||
projectSpinner?.fail()
|
||||
|
||||
logger.info("")
|
||||
if (errors[ERRORS.MISSING_DIR_OR_EMPTY_PROJECT]) {
|
||||
logger.error(`The path ${chalk.cyan(cwd)} does not exist or is empty.`)
|
||||
}
|
||||
|
||||
if (errors[ERRORS.EXISTING_CONFIG]) {
|
||||
logger.error(
|
||||
`A components.json file already exists at ${chalk.cyan(
|
||||
cwd
|
||||
)}.\nTo start over, remove the components.json file and try again.`
|
||||
)
|
||||
}
|
||||
|
||||
logger.info("")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
projectSpinner?.succeed("Running preflight checks.")
|
||||
|
||||
const projectInfo = await getProjectInfo(cwd)
|
||||
|
||||
if (!projectInfo?.tailwindConfigFile || !projectInfo?.tailwindCssFile) {
|
||||
return {
|
||||
error: ERRORS.TAILWIND_NOT_CONFIGURED,
|
||||
info: projectInfo,
|
||||
}
|
||||
const frameworkSpinner = ora(`Checking for framework...`).start()
|
||||
if (!projectInfo?.framework) {
|
||||
errors[ERRORS.UNSUPPORTED_FRAMEWORK] = true
|
||||
frameworkSpinner?.fail()
|
||||
logger.info("")
|
||||
logger.error(
|
||||
`We couldn't detect a supported framework at ${chalk.cyan(cwd)}.\n` +
|
||||
`Visit ${chalk.cyan(
|
||||
"https://ui.shadcn.com/docs/installation/manual"
|
||||
)} to manually create a components.json file.\n`
|
||||
)
|
||||
logger.info("")
|
||||
process.exit(1)
|
||||
} else {
|
||||
frameworkSpinner?.succeed("Checking for framework.")
|
||||
}
|
||||
|
||||
if (!projectInfo.tsConfigAliasPrefix) {
|
||||
return {
|
||||
error: ERRORS.IMPORT_ALIAS_MISSING,
|
||||
info: projectInfo,
|
||||
}
|
||||
const tailwindSpinner = ora(`Checking for Tailwind CSS...`).start()
|
||||
if (!projectInfo?.tailwindConfigFile || !projectInfo?.tailwindCssFile) {
|
||||
errors[ERRORS.TAILWIND_NOT_CONFIGURED] = true
|
||||
tailwindSpinner?.fail()
|
||||
} else {
|
||||
tailwindSpinner?.succeed("Checking for Tailwind CSS.")
|
||||
}
|
||||
|
||||
const tsConfigSpinner = ora(`Checking for import alias...`).start()
|
||||
if (!projectInfo?.aliasPrefix) {
|
||||
errors[ERRORS.IMPORT_ALIAS_MISSING] = true
|
||||
tsConfigSpinner?.fail()
|
||||
} else {
|
||||
tsConfigSpinner?.succeed("Checking for import alias.")
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
logger.info("")
|
||||
|
||||
if (errors[ERRORS.TAILWIND_NOT_CONFIGURED]) {
|
||||
const framework =
|
||||
projectInfo?.framework &&
|
||||
["next-app", "next-pages"].includes(projectInfo?.framework)
|
||||
? "nextjs"
|
||||
: projectInfo?.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 ${chalk.cyan(tailwindInstallationUrl)} to get started.\n`
|
||||
)
|
||||
}
|
||||
|
||||
if (errors[ERRORS.IMPORT_ALIAS_MISSING]) {
|
||||
const framework =
|
||||
projectInfo?.framework &&
|
||||
["next-app", "next-pages"].includes(projectInfo?.framework)
|
||||
? "next"
|
||||
: projectInfo?.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.info("")
|
||||
process.exit(1)
|
||||
}
|
||||
logger.info("")
|
||||
|
||||
return {
|
||||
error: null,
|
||||
info: projectInfo,
|
||||
errors,
|
||||
projectInfo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,88 +9,96 @@ describe("get project info", async () => {
|
||||
name: "next-app",
|
||||
type: {
|
||||
framework: "next-app",
|
||||
isUsingSrcDir: false,
|
||||
isTypescript: true,
|
||||
isSrcDir: false,
|
||||
isRSC: true,
|
||||
isTsx: true,
|
||||
tailwindConfigFile: "tailwind.config.ts",
|
||||
tailwindCssFile: "app/globals.css",
|
||||
tsConfigAliasPrefix: "@",
|
||||
aliasPrefix: "@",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "next-app-src",
|
||||
type: {
|
||||
framework: "next-app",
|
||||
isUsingSrcDir: true,
|
||||
isTypescript: true,
|
||||
isSrcDir: true,
|
||||
isRSC: true,
|
||||
isTsx: true,
|
||||
tailwindConfigFile: "tailwind.config.ts",
|
||||
tailwindCssFile: "src/app/styles.css",
|
||||
tsConfigAliasPrefix: "#",
|
||||
aliasPrefix: "#",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "next-pages",
|
||||
type: {
|
||||
framework: "next-pages",
|
||||
isUsingSrcDir: false,
|
||||
isTypescript: true,
|
||||
isSrcDir: false,
|
||||
isRSC: false,
|
||||
isTsx: true,
|
||||
tailwindConfigFile: "tailwind.config.ts",
|
||||
tailwindCssFile: "styles/globals.css",
|
||||
tsConfigAliasPrefix: "~",
|
||||
aliasPrefix: "~",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "next-pages-src",
|
||||
type: {
|
||||
framework: "next-pages",
|
||||
isUsingSrcDir: true,
|
||||
isTypescript: true,
|
||||
isSrcDir: true,
|
||||
isRSC: false,
|
||||
isTsx: true,
|
||||
tailwindConfigFile: "tailwind.config.ts",
|
||||
tailwindCssFile: "src/styles/globals.css",
|
||||
tsConfigAliasPrefix: "@",
|
||||
aliasPrefix: "@",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "t3-app",
|
||||
type: {
|
||||
framework: "next-app",
|
||||
isUsingSrcDir: true,
|
||||
isTypescript: true,
|
||||
isSrcDir: true,
|
||||
isRSC: true,
|
||||
isTsx: true,
|
||||
tailwindConfigFile: "tailwind.config.ts",
|
||||
tailwindCssFile: "src/styles/globals.css",
|
||||
tsConfigAliasPrefix: "~",
|
||||
aliasPrefix: "~",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "t3-pages",
|
||||
type: {
|
||||
framework: "next-pages",
|
||||
isUsingSrcDir: true,
|
||||
isTypescript: true,
|
||||
isSrcDir: true,
|
||||
isRSC: false,
|
||||
isTsx: true,
|
||||
tailwindConfigFile: "tailwind.config.ts",
|
||||
tailwindCssFile: "src/styles/globals.css",
|
||||
tsConfigAliasPrefix: "~",
|
||||
aliasPrefix: "~",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remix",
|
||||
type: {
|
||||
framework: "remix",
|
||||
isUsingSrcDir: false,
|
||||
isTypescript: true,
|
||||
isSrcDir: false,
|
||||
isRSC: false,
|
||||
isTsx: true,
|
||||
tailwindConfigFile: "tailwind.config.ts",
|
||||
tailwindCssFile: "app/tailwind.css",
|
||||
tsConfigAliasPrefix: "~",
|
||||
aliasPrefix: "~",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "vite",
|
||||
type: {
|
||||
framework: "vite",
|
||||
isUsingSrcDir: true,
|
||||
isTypescript: true,
|
||||
isSrcDir: true,
|
||||
isRSC: false,
|
||||
isTsx: true,
|
||||
tailwindConfigFile: "tailwind.config.js",
|
||||
tailwindCssFile: "src/index.css",
|
||||
tsConfigAliasPrefix: null,
|
||||
aliasPrefix: null,
|
||||
},
|
||||
},
|
||||
])(`getProjectType($name) -> $type`, async ({ name, type }) => {
|
||||
|
||||
Reference in New Issue
Block a user