diff --git a/.changeset/odd-pigs-cross.md b/.changeset/odd-pigs-cross.md new file mode 100644 index 00000000000..4f069450e4c --- /dev/null +++ b/.changeset/odd-pigs-cross.md @@ -0,0 +1,5 @@ +--- +"shadcn-ui": minor +--- + +add support for custom ui dir diff --git a/apps/www/content/docs/components-json.mdx b/apps/www/content/docs/components-json.mdx index 34567177803..625a1474447 100644 --- a/apps/www/content/docs/components-json.mdx +++ b/apps/www/content/docs/components-json.mdx @@ -173,3 +173,17 @@ Import alias for your components. } } ``` + +### aliases.ui + +Import alias for `ui` components. + +The CLI will use the `aliases.ui` value to determine where to place your `ui` components. Use this config if you want to customize the installation directory for your `ui` components. + +```json title="components.json" +{ + "aliases": { + "ui": "@/app/ui" + } +} +``` diff --git a/apps/www/public/schema.json b/apps/www/public/schema.json index 1d310f2adc9..71297ad3f75 100644 --- a/apps/www/public/schema.json +++ b/apps/www/public/schema.json @@ -41,6 +41,9 @@ }, "components": { "type": "string" + }, + "ui": { + "type": "string" } }, "required": ["utils", "components"] diff --git a/packages/cli/src/utils/get-config.ts b/packages/cli/src/utils/get-config.ts index 3aac9378049..18978018a60 100644 --- a/packages/cli/src/utils/get-config.ts +++ b/packages/cli/src/utils/get-config.ts @@ -33,6 +33,7 @@ export const rawConfigSchema = z aliases: z.object({ components: z.string(), utils: z.string(), + ui: z.string().optional(), }), }) .strict() @@ -45,6 +46,7 @@ export const configSchema = rawConfigSchema.extend({ tailwindCss: z.string(), utils: z.string(), components: z.string(), + ui: z.string(), }), }) @@ -79,6 +81,9 @@ export async function resolveConfigPaths(cwd: string, config: RawConfig) { tailwindCss: path.resolve(cwd, config.tailwind.css), utils: await resolveImport(config.aliases["utils"], tsConfig), components: await resolveImport(config.aliases["components"], tsConfig), + ui: config.aliases["ui"] + ? await resolveImport(config.aliases["ui"], tsConfig) + : await resolveImport(config.aliases["components"], tsConfig), }, }) } diff --git a/packages/cli/src/utils/registry/index.ts b/packages/cli/src/utils/registry/index.ts index a16e77ef347..5fd5450a446 100644 --- a/packages/cli/src/utils/registry/index.ts +++ b/packages/cli/src/utils/registry/index.ts @@ -117,11 +117,14 @@ export async function getItemTargetPath( item: Pick, "type">, override?: string ) { - // Allow overrides for all items but ui. - if (override && item.type !== "components:ui") { + if (override) { return override } + if (item.type === "components:ui" && config.aliases.ui) { + return config.resolvedPaths.ui + } + const [parent, type] = item.type.split(":") if (!(parent in config.resolvedPaths)) { return null diff --git a/packages/cli/src/utils/transformers/transform-import.ts b/packages/cli/src/utils/transformers/transform-import.ts index 51ec64b6adf..0b912001ea1 100644 --- a/packages/cli/src/utils/transformers/transform-import.ts +++ b/packages/cli/src/utils/transformers/transform-import.ts @@ -8,12 +8,18 @@ export const transformImport: Transformer = async ({ sourceFile, config }) => { // Replace @/registry/[style] with the components alias. if (moduleSpecifier.startsWith("@/registry/")) { - importDeclaration.setModuleSpecifier( - moduleSpecifier.replace( - /^@\/registry\/[^/]+/, - config.aliases.components + if (config.aliases.ui) { + importDeclaration.setModuleSpecifier( + moduleSpecifier.replace(/^@\/registry\/[^/]+\/ui/, config.aliases.ui) + ) + } else { + importDeclaration.setModuleSpecifier( + moduleSpecifier.replace( + /^@\/registry\/[^/]+/, + config.aliases.components + ) ) - ) + } } // Replace `import { cn } from "@/lib/utils"` diff --git a/packages/cli/test/fixtures/config-ui/components.json b/packages/cli/test/fixtures/config-ui/components.json new file mode 100644 index 00000000000..0feb828e35e --- /dev/null +++ b/packages/cli/test/fixtures/config-ui/components.json @@ -0,0 +1,16 @@ +{ + "style": "new-york", + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "tw-" + }, + "rsc": false, + "aliases": { + "utils": "~/lib/utils", + "components": "~/components", + "ui": "~/ui" + } +} diff --git a/packages/cli/test/fixtures/config-ui/package.json b/packages/cli/test/fixtures/config-ui/package.json new file mode 100644 index 00000000000..673ee87c956 --- /dev/null +++ b/packages/cli/test/fixtures/config-ui/package.json @@ -0,0 +1,7 @@ +{ + "name": "test-cli-config-ui", + "version": "1.0.0", + "main": "index.js", + "author": "shadcn", + "license": "MIT" +} diff --git a/packages/cli/test/fixtures/config-ui/tsconfig.json b/packages/cli/test/fixtures/config-ui/tsconfig.json new file mode 100644 index 00000000000..03ebb748aef --- /dev/null +++ b/packages/cli/test/fixtures/config-ui/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "es2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "checkJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "noUncheckedIndexedAccess": true, + "baseUrl": ".", + "paths": { + "~/*": ["./src/*"] + } + }, + "include": [ + ".eslintrc.cjs", + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.cjs", + "**/*.mjs" + ], + "exclude": ["node_modules"] +} diff --git a/packages/cli/test/utils/__snapshots__/transform-import.test.ts.snap b/packages/cli/test/utils/__snapshots__/transform-import.test.ts.snap index ff6a660bc44..f903405626d 100644 --- a/packages/cli/test/utils/__snapshots__/transform-import.test.ts.snap +++ b/packages/cli/test/utils/__snapshots__/transform-import.test.ts.snap @@ -34,3 +34,27 @@ import { Foo } from \\"bar\\" import { bar } from \\"@/lib/utils/bar\\" " `; + +exports[`transform import 4`] = ` +"import * as React from \\"react\\" +import { Foo } from \\"bar\\" + import { Button } from \\"~/src/components/button\\" + import { Label} from \\"ui/label\\" + import { Box } from \\"@/registry/new-york/box\\" + + import { cn } from \\"~/src/utils\\" + import { bar } from \\"@/lib/utils/bar\\" + " +`; + +exports[`transform import 5`] = ` +"import * as React from \\"react\\" +import { Foo } from \\"bar\\" + import { Button } from \\"~/src/ui/button\\" + import { Label} from \\"ui/label\\" + import { Box } from \\"@/registry/new-york/box\\" + + import { cn } from \\"~/src/utils\\" + import { bar } from \\"@/lib/utils/bar\\" + " +`; diff --git a/packages/cli/test/utils/get-config.test.ts b/packages/cli/test/utils/get-config.test.ts index bb0092fbef1..6229d475f2e 100644 --- a/packages/cli/test/utils/get-config.test.ts +++ b/packages/cli/test/utils/get-config.test.ts @@ -77,6 +77,7 @@ test("get config", async () => { "../fixtures/config-partial", "./lib/utils" ), + ui: path.resolve(__dirname, "../fixtures/config-partial", "./components"), }, }) @@ -91,7 +92,7 @@ test("get config", async () => { baseColor: "zinc", css: "src/app/globals.css", cssVariables: true, - prefix: "tw-" + prefix: "tw-", }, aliases: { components: "~/components", @@ -113,6 +114,11 @@ test("get config", async () => { "../fixtures/config-full", "./src/components" ), + ui: path.resolve( + __dirname, + "../fixtures/config-full", + "./src/components" + ), utils: path.resolve( __dirname, "../fixtures/config-full", @@ -153,6 +159,7 @@ test("get config", async () => { "../fixtures/config-jsx", "./components" ), + ui: path.resolve(__dirname, "../fixtures/config-jsx", "./components"), utils: path.resolve(__dirname, "../fixtures/config-jsx", "./lib/utils"), }, }) diff --git a/packages/cli/test/utils/get-item-target-path.test.ts b/packages/cli/test/utils/get-item-target-path.test.ts new file mode 100644 index 00000000000..5fe0ecf437f --- /dev/null +++ b/packages/cli/test/utils/get-item-target-path.test.ts @@ -0,0 +1,39 @@ +import path from "path" +import { expect, test } from "vitest" + +import { getConfig } from "../../src/utils/get-config" +import { getItemTargetPath } from "../../src/utils/registry" + +test("get item target path", async () => { + // Full config. + let appDir = path.resolve(__dirname, "../fixtures/config-full") + expect( + await getItemTargetPath(await getConfig(appDir), { + type: "components:ui", + }) + ).toEqual(path.resolve(appDir, "./src/components/ui")) + + // Partial config. + appDir = path.resolve(__dirname, "../fixtures/config-partial") + expect( + await getItemTargetPath(await getConfig(appDir), { + type: "components:ui", + }) + ).toEqual(path.resolve(appDir, "./components/ui")) + + // JSX. + appDir = path.resolve(__dirname, "../fixtures/config-jsx") + expect( + await getItemTargetPath(await getConfig(appDir), { + type: "components:ui", + }) + ).toEqual(path.resolve(appDir, "./components/ui")) + + // Custom paths. + appDir = path.resolve(__dirname, "../fixtures/config-ui") + expect( + await getItemTargetPath(await getConfig(appDir), { + type: "components:ui", + }) + ).toEqual(path.resolve(appDir, "./src/ui")) +}) diff --git a/packages/cli/test/utils/transform-import.test.ts b/packages/cli/test/utils/transform-import.test.ts index 4c36cd25056..a7ad3e8306b 100644 --- a/packages/cli/test/utils/transform-import.test.ts +++ b/packages/cli/test/utils/transform-import.test.ts @@ -71,4 +71,50 @@ import { Foo } from "bar" }, }) ).toMatchSnapshot() + + expect( + await transform({ + filename: "test.ts", + raw: `import * as React from "react" +import { Foo } from "bar" + import { Button } from "@/registry/new-york/ui/button" + import { Label} from "ui/label" + import { Box } from "@/registry/new-york/box" + + import { cn } from "@/lib/utils" + import { bar } from "@/lib/utils/bar" + `, + config: { + tsx: true, + aliases: { + components: "~/src/components", + utils: "~/src/utils", + ui: "~/src/components", + }, + }, + }) + ).toMatchSnapshot() + + expect( + await transform({ + filename: "test.ts", + raw: `import * as React from "react" +import { Foo } from "bar" + import { Button } from "@/registry/new-york/ui/button" + import { Label} from "ui/label" + import { Box } from "@/registry/new-york/box" + + import { cn } from "@/lib/utils" + import { bar } from "@/lib/utils/bar" + `, + config: { + tsx: true, + aliases: { + components: "~/src/components", + utils: "~/src/utils", + ui: "~/src/ui", + }, + }, + }) + ).toMatchSnapshot() })