feat(cli): update preflight handling

This commit is contained in:
shadcn
2024-08-14 16:53:20 +04:00
parent 99eb4a2df7
commit 665d4e8f93
5 changed files with 155 additions and 112 deletions

View File

@@ -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)

View File

@@ -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"

View File

@@ -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`,
},
}

View File

@@ -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,
}
}

View File

@@ -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 }) => {