From 665d4e8f9328cd1450bfbb5fb864d00b68cfb248 Mon Sep 17 00:00:00 2001 From: shadcn Date: Wed, 14 Aug 2024 16:53:20 +0400 Subject: [PATCH] feat(cli): update preflight handling --- packages/cli/src/commands/init.ts | 47 +------ packages/cli/src/utils/errors.ts | 6 +- packages/cli/src/utils/get-project-info.ts | 33 ++--- packages/cli/src/utils/preflight.ts | 125 ++++++++++++++---- .../cli/test/utils/get-project-info.test.ts | 56 ++++---- 5 files changed, 155 insertions(+), 112 deletions(-) diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 5306077460..ffb327cf68 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -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) diff --git a/packages/cli/src/utils/errors.ts b/packages/cli/src/utils/errors.ts index a7d438bea9..6d9cc6ddfc 100644 --- a/packages/cli/src/utils/errors.ts +++ b/packages/cli/src/utils/errors.ts @@ -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" diff --git a/packages/cli/src/utils/get-project-info.ts b/packages/cli/src/utils/get-project-info.ts index 06028cba0b..5e72763d4a 100644 --- a/packages/cli/src/utils/get-project-info.ts +++ b/packages/cli/src/utils/get-project-info.ts @@ -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 { 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 { ]) 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 { 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 { 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 { 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 { prefix: "", }, aliases: { - utils: `${projectInfo.tsConfigAliasPrefix}/lib/utils`, - components: `${projectInfo.tsConfigAliasPrefix}/components`, + utils: `${projectInfo.aliasPrefix}/lib/utils`, + components: `${projectInfo.aliasPrefix}/components`, }, } diff --git a/packages/cli/src/utils/preflight.ts b/packages/cli/src/utils/preflight.ts index 8844bdef0b..69ac3ae447 100644 --- a/packages/cli/src/utils/preflight.ts +++ b/packages/cli/src/utils/preflight.ts @@ -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 = {} + // 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, } } diff --git a/packages/cli/test/utils/get-project-info.test.ts b/packages/cli/test/utils/get-project-info.test.ts index fa9fc88818..00f8d666c5 100644 --- a/packages/cli/test/utils/get-project-info.test.ts +++ b/packages/cli/test/utils/get-project-info.test.ts @@ -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 }) => {