diff --git a/packages/cli/package.json b/packages/cli/package.json index 4ce046d1d6..6ead7a58b6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -45,6 +45,7 @@ "dependencies": { "chalk": "5.2.0", "commander": "^10.0.0", + "cosmiconfig": "^8.1.3", "execa": "^7.0.0", "fs-extra": "^11.1.0", "https-proxy-agent": "^6.2.0", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index dda137a1fd..75626ac119 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -7,6 +7,7 @@ import ora from "ora" import prompts from "prompts" import { Component, getAvailableComponents } from "./utils/get-components" +import { Config, getCliConfig } from "./utils/get-config" import { getPackageInfo } from "./utils/get-package-info" import { getPackageManager } from "./utils/get-package-manager" import { getProjectInfo } from "./utils/get-project-info" @@ -27,6 +28,8 @@ const PROJECT_DEPENDENCIES = [ async function main() { const packageInfo = await getPackageInfo() const projectInfo = await getProjectInfo() + const cliConfig = await getCliConfig() + const packageManager = getPackageManager() const program = new Command() @@ -144,11 +147,7 @@ async function main() { selectedComponents = await promptForComponents(availableComponents) } - const dir = await promptForDestinationDir( - projectInfo?.srcComponentsUiDir - ? "./src/components/ui" - : "./components/ui" - ) + const dir = await promptForDestinationDir(cliConfig) if (!selectedComponents?.length) { logger.warn("No components selected. Nothing to install.") @@ -171,10 +170,16 @@ async function main() { // Write the files. for (const file of component.files) { - // Replace alias with the project's alias. - if (projectInfo?.alias) { - file.content = file.content.replace(/@\//g, projectInfo.alias) - } + // because these are the predefined routes for the utils and components we can + // use them as a replacer for the defined routes on the installed file. + file.content = file.content.replace( + "@/lib/utils", + cliConfig.utilsLocation + ) + file.content = file.content.replace( + "@/components/ui/", + cliConfig.componentDirAlias + ) const filePath = path.resolve(dir, file.name) await fs.writeFile(filePath, file.content) @@ -210,13 +215,17 @@ async function promptForComponents(components: Component[]) { return selectedComponents } -async function promptForDestinationDir(installDir = "./components/ui") { +async function promptForDestinationDir(cliConfig: Config) { + if (!cliConfig.askForDir) { + return cliConfig.componentsDirInstallation + } + const { dir } = await prompts([ { type: "text", name: "dir", message: "Where would you like to install the component(s)?", - initial: installDir, + initial: cliConfig.componentsDirInstallation, }, ]) diff --git a/packages/cli/src/utils/get-config.ts b/packages/cli/src/utils/get-config.ts new file mode 100644 index 0000000000..ac518f260d --- /dev/null +++ b/packages/cli/src/utils/get-config.ts @@ -0,0 +1,63 @@ +import { cosmiconfig } from "cosmiconfig" +import * as z from "zod" + +export const COMPONENTS_DIR = "./components/ui/" +export const UTILS_LOCATION = "@/lib/utils" +export const COMPONENT_ALIAS = "@/components/ui/" + +/** + * this is the name of the key we are looking for, the following are the intended values to look for: + * - shadcn-ui property in package.json + * - .shadcn-uirc file in JSON or YAML format + * - .shadcn-uirc.json, .shadcn-uirc.yaml, .shadcn-uirc.yml, .shadcn-uirc.js, or .shadcn-uirc.cjs file + * - shadcn-uirc, shadcn-uirc.json, shadcn-uirc.yaml, shadcn-uirc.yml, shadcn-uirc.js or shadcn-uirc.cjs file inside a .config subdirectory + * - shadcn-ui.config.js or shadcn-ui.config.cjs CommonJS module exporting an object + */ +const explorer = cosmiconfig("shadcn-ui") + +const configSchema = z.object({ + componentsDirInstallation: z.string(), + askForDir: z.boolean(), + utilsLocation: z.string(), + componentDirAlias: z.string(), +}) + +export type Config = z.infer + +export async function getCliConfig(): Promise { + const defaultConfig: Config = { + componentsDirInstallation: COMPONENTS_DIR, + askForDir: true, + utilsLocation: UTILS_LOCATION, + componentDirAlias: COMPONENT_ALIAS, + } + + const userDefinedConfig = await getConfigFromEverywhere() + + return { + ...defaultConfig, + ...userDefinedConfig, + askForDir: !userDefinedConfig.componentsDirInstallation, + } +} + +export async function getConfigFromEverywhere() { + try { + const configResult = await explorer.search() + + if (!configResult) { + // we should always return an object so we can then merge with + // the base config + return {} + } + + const { config } = configResult + + const parsedConfig = configSchema.partial().parse(config) + return parsedConfig + } catch (e) { + // lets just show the error to the user to aware about the issue, but lets not handle it. + console.log(e) + return {} + } +} diff --git a/packages/cli/src/utils/get-project-info.ts b/packages/cli/src/utils/get-project-info.ts index eb451aa6e1..7f361f8ca1 100644 --- a/packages/cli/src/utils/get-project-info.ts +++ b/packages/cli/src/utils/get-project-info.ts @@ -5,7 +5,6 @@ import fs from "fs-extra" export async function getProjectInfo() { const info = { tsconfig: null, - alias: null, srcDir: false, appDir: false, srcComponentsUiDir: false, @@ -14,12 +13,9 @@ export async function getProjectInfo() { try { const tsconfig = await getTsConfig() - const paths = tsconfig?.compilerOptions?.paths - const alias = paths ? Object.keys(paths)[0].replace("*", "") : null return { tsconfig, - alias, srcDir: existsSync(path.resolve("./src")), appDir: existsSync(path.resolve("./app")) || diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 422d936fe0..16fd130d17 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -317,6 +317,9 @@ importers: commander: specifier: ^10.0.0 version: 10.0.0 + cosmiconfig: + specifier: ^8.1.3 + version: 8.1.3 execa: specifier: ^7.0.0 version: 7.0.0