Skip to content

Commit

Permalink
feat(cli): add support for custom ui dir (#2266)
Browse files Browse the repository at this point in the history
* feat(cli): add support for custom ui dir

* docs(www): update docs for aliases.ui

* chore: add changeset
  • Loading branch information
shadcn committed Jan 2, 2024
1 parent 98859e7 commit be580db
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/odd-pigs-cross.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"shadcn-ui": minor
---

add support for custom ui dir
14 changes: 14 additions & 0 deletions apps/www/content/docs/components-json.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
```
3 changes: 3 additions & 0 deletions apps/www/public/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
},
"components": {
"type": "string"
},
"ui": {
"type": "string"
}
},
"required": ["utils", "components"]
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/utils/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const rawConfigSchema = z
aliases: z.object({
components: z.string(),
utils: z.string(),
ui: z.string().optional(),
}),
})
.strict()
Expand All @@ -45,6 +46,7 @@ export const configSchema = rawConfigSchema.extend({
tailwindCss: z.string(),
utils: z.string(),
components: z.string(),
ui: z.string(),
}),
})

Expand Down Expand Up @@ -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),
},
})
}
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/utils/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,11 +117,14 @@ export async function getItemTargetPath(
item: Pick<z.infer<typeof registryItemWithContentSchema>, "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
Expand Down
16 changes: 11 additions & 5 deletions packages/cli/src/utils/transformers/transform-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/test/fixtures/config-ui/components.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
7 changes: 7 additions & 0 deletions packages/cli/test/fixtures/config-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "test-cli-config-ui",
"version": "1.0.0",
"main": "index.js",
"author": "shadcn",
"license": "MIT"
}
33 changes: 33 additions & 0 deletions packages/cli/test/fixtures/config-ui/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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\\"
"
`;
9 changes: 8 additions & 1 deletion packages/cli/test/utils/get-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ test("get config", async () => {
"../fixtures/config-partial",
"./lib/utils"
),
ui: path.resolve(__dirname, "../fixtures/config-partial", "./components"),
},
})

Expand All @@ -91,7 +92,7 @@ test("get config", async () => {
baseColor: "zinc",
css: "src/app/globals.css",
cssVariables: true,
prefix: "tw-"
prefix: "tw-",
},
aliases: {
components: "~/components",
Expand All @@ -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",
Expand Down Expand Up @@ -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"),
},
})
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/test/utils/get-item-target-path.test.ts
Original file line number Diff line number Diff line change
@@ -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"))
})
46 changes: 46 additions & 0 deletions packages/cli/test/utils/transform-import.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})

1 comment on commit be580db

@vercel
Copy link

@vercel vercel bot commented on be580db Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

ui – ./apps/www

ui.shadcn.com
example-playground.vercel.app
ui-git-main-shadcn-pro.vercel.app
ui-shadcn-pro.vercel.app

Please sign in to comment.