diff --git a/.changeset/stupid-fans-type.md b/.changeset/stupid-fans-type.md
new file mode 100644
index 000000000..4f7b4c707
--- /dev/null
+++ b/.changeset/stupid-fans-type.md
@@ -0,0 +1,5 @@
+---
+"shadcn": minor
+---
+
+allow alias placeholders in target for registry items
diff --git a/apps/v4/content/docs/registry/examples.mdx b/apps/v4/content/docs/registry/examples.mdx
index a537a4a50..a71ce6527 100644
--- a/apps/v4/content/docs/registry/examples.mdx
+++ b/apps/v4/content/docs/registry/examples.mdx
@@ -309,6 +309,94 @@ A `registry:hook` item is a custom React hook.
}
```
+## Target Placeholders
+
+Use `files[].target` placeholders when a registry item should install files
+under the user's configured shadcn directories. The available placeholders are
+`@components/`, `@ui/`, `@lib/` and `@hooks/`.
+
+The placeholders are resolved from `components.json`, so the same registry item
+works in projects using `@/`, custom TypeScript aliases, package imports or
+workspace package exports.
+
+Anything after the placeholder is preserved. For example,
+`@ui/ai/prompt-input.tsx` installs under the user's configured `ui` directory
+at `ai/prompt-input.tsx`.
+
+```json title="alias-child.json" showLineNumbers {9,15,21}
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "alias-child",
+ "type": "registry:item",
+ "files": [
+ {
+ "path": "registry/new-york/alias/target-alias-button.tsx",
+ "type": "registry:ui",
+ "target": "@ui/target-alias-button.tsx",
+ "content": "..."
+ },
+ {
+ "path": "registry/new-york/alias/target-alias-helper.ts",
+ "type": "registry:lib",
+ "target": "@lib/target-alias-helper.ts",
+ "content": "..."
+ },
+ {
+ "path": "registry/new-york/alias/prompt-input.tsx",
+ "type": "registry:ui",
+ "target": "@ui/ai/prompt-input.tsx",
+ "content": "..."
+ }
+ ]
+}
+```
+
+Registry dependencies can use target placeholders too. In the following example,
+the child item installs a UI component and a helper, while the parent item
+installs an app component and a hook.
+
+```json title="alias-parent.json" showLineNumbers {7,13}
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "alias-parent",
+ "type": "registry:item",
+ "registryDependencies": ["https://example.com/r/alias-child.json"],
+ "files": [
+ {
+ "path": "registry/new-york/alias/target-alias-panel.tsx",
+ "type": "registry:component",
+ "target": "@components/target-alias-panel.tsx",
+ "content": "..."
+ },
+ {
+ "path": "registry/new-york/alias/use-target-alias.ts",
+ "type": "registry:hook",
+ "target": "@hooks/use-target-alias.ts",
+ "content": "..."
+ }
+ ]
+}
+```
+
+The `target` controls where the file is written, even when it differs from the
+file `type`.
+
+```json title="type-mismatch.json" showLineNumbers {9}
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "type-mismatch",
+ "type": "registry:item",
+ "files": [
+ {
+ "path": "registry/new-york/example/format-date.ts",
+ "type": "registry:ui",
+ "target": "@lib/format-date.ts",
+ "content": "..."
+ }
+ ]
+}
+```
+
## registry:font
### Custom font
diff --git a/apps/v4/content/docs/registry/registry-item-json.mdx b/apps/v4/content/docs/registry/registry-item-json.mdx
index e80c7c482..f7badeee0 100644
--- a/apps/v4/content/docs/registry/registry-item-json.mdx
+++ b/apps/v4/content/docs/registry/registry-item-json.mdx
@@ -226,6 +226,80 @@ By default, the `shadcn` cli will read a project's `components.json` file to det
Use `~` to refer to the root of the project e.g `~/foo.config.js`.
+You can also use registry target placeholders to place files under the
+directories configured by the user's `components.json`. These placeholders are
+only supported at the start of `target` and are independent of the project's
+import prefix. For example, `@ui/button.tsx` works whether the project imports
+components with `@/`, `#`, package imports or workspace exports.
+
+| Placeholder | Resolves to |
+| -------------- | -------------------- |
+| `@components/` | `aliases.components` |
+| `@ui/` | `aliases.ui` |
+| `@lib/` | `aliases.lib` |
+| `@hooks/` | `aliases.hooks` |
+
+Use these placeholders when you want a registry item to install into the
+project's configured shadcn directories without hardcoding `components`, `src`
+or workspace package paths. Anything after the placeholder is preserved, so
+`@ui/ai/prompt-input.tsx` installs under the user's configured `ui` directory
+at `ai/prompt-input.tsx`.
+
+```json title="registry-item.json" showLineNumbers
+{
+ "files": [
+ {
+ "path": "registry/new-york/example/button.tsx",
+ "type": "registry:ui",
+ "target": "@ui/button.tsx"
+ },
+ {
+ "path": "registry/new-york/example/prompt-input.tsx",
+ "type": "registry:ui",
+ "target": "@ui/ai/prompt-input.tsx"
+ },
+ {
+ "path": "registry/new-york/example/card.tsx",
+ "type": "registry:component",
+ "target": "@components/card.tsx"
+ },
+ {
+ "path": "registry/new-york/example/helper.ts",
+ "type": "registry:lib",
+ "target": "@lib/helper.ts"
+ },
+ {
+ "path": "registry/new-york/example/use-demo.ts",
+ "type": "registry:hook",
+ "target": "@hooks/use-demo.ts"
+ }
+ ]
+}
+```
+
+The `target` property decides where the file is written. It can point to a
+different shadcn directory than the file `type`.
+
+```json title="registry-item.json" showLineNumbers
+{
+ "files": [
+ {
+ "path": "registry/new-york/example/format-date.ts",
+ "type": "registry:ui",
+ "target": "@lib/format-date.ts"
+ }
+ ]
+}
+```
+
+Unknown placeholders are treated as regular target paths. For example,
+`@foo/bar.ts` is written as `foo/bar.ts`. Embedded placeholders such as
+`components/@ui/button.tsx` are also treated as regular paths.
+
+
+ `@utils/` is not supported because `utils` points to a file, not a directory.
+
+
### tailwind
**DEPRECATED:** Use `cssVars.theme` instead for Tailwind v4 projects.
diff --git a/apps/v4/public/schema/registry-item.json b/apps/v4/public/schema/registry-item.json
index 110c7b9ce..9255c6422 100644
--- a/apps/v4/public/schema/registry-item.json
+++ b/apps/v4/public/schema/registry-item.json
@@ -90,7 +90,7 @@
},
"target": {
"type": "string",
- "description": "The target path of the file. This is the path to the file in the project."
+ "description": "The target path of the file. This is the path to the file in the project. Supports registry target placeholders @components/, @ui/, @lib/, and @hooks/, which resolve to the corresponding aliases configured in components.json. These placeholders are independent of the project's import prefix."
}
},
"if": {
diff --git a/packages/shadcn/src/utils/add-components.ts b/packages/shadcn/src/utils/add-components.ts
index d6510227b..62252d2e1 100644
--- a/packages/shadcn/src/utils/add-components.ts
+++ b/packages/shadcn/src/utils/add-components.ts
@@ -254,36 +254,52 @@ async function addWorkspaceComponents(
silent: true,
})
- // 5. Group files by their type to determine target config and update files.
- const filesByType = new Map()
-
- for (const file of tree.files ?? []) {
- const type = file.type || "registry:ui"
- if (!filesByType.has(type)) {
- filesByType.set(type, [])
- }
- filesByType.get(type)!.push(file)
- }
-
- const FILE_TYPE_TO_CONFIG_KEY: Record = {
+ // 5. Group files by their target config and update files.
+ const TARGET_ALIAS_KEYS = ["components", "ui", "lib", "hooks"] as const
+ type TargetAliasKey = (typeof TARGET_ALIAS_KEYS)[number]
+ const filesByTarget = new Map()
+ const FILE_TYPE_TO_CONFIG_KEY: Record = {
"registry:ui": "ui",
"registry:hook": "hooks",
"registry:lib": "lib",
}
- const getTargetConfigForType = (type: string) => {
- const configKey = FILE_TYPE_TO_CONFIG_KEY[type]
+ const isTargetAliasKey = (key: string): key is TargetAliasKey => {
+ return TARGET_ALIAS_KEYS.includes(key as TargetAliasKey)
+ }
+ const getTargetAliasKey = (target?: string) => {
+ const match = target?.match(/^@([^/]+)\//)
+ return match && isTargetAliasKey(match[1]) ? match[1] : null
+ }
+ const getTargetConfigKeyForFile = (
+ file: z.infer
+ ) => {
+ return (
+ getTargetAliasKey(file.target) ??
+ FILE_TYPE_TO_CONFIG_KEY[file.type || "registry:ui"] ??
+ "components"
+ )
+ }
+ const getTargetConfigForKey = (configKey: TargetAliasKey) => {
return configKey && workspaceConfig[configKey]
? workspaceConfig[configKey]
: config
}
- // Process each type of component with its appropriate target config.
- for (const type of Array.from(filesByType.keys())) {
- const typeFiles = filesByType.get(type)!
- const targetConfig = getTargetConfigForType(type)
+ for (const file of tree.files ?? []) {
+ const targetKey = getTargetConfigKeyForFile(file)
+ if (!filesByTarget.has(targetKey)) {
+ filesByTarget.set(targetKey, [])
+ }
+ filesByTarget.get(targetKey)!.push(file)
+ }
+
+ // Process each target config with its appropriate workspace config.
+ for (const targetKey of Array.from(filesByTarget.keys())) {
+ const targetFiles = filesByTarget.get(targetKey)!
+ const targetConfig = getTargetConfigForKey(targetKey)
const plannedFiles = (tree.files ?? []).filter((file) => {
- const fileTargetConfig = getTargetConfigForType(
- file.type || "registry:ui"
+ const fileTargetConfig = getTargetConfigForKey(
+ getTargetConfigKeyForFile(file)
)
return (
@@ -301,8 +317,8 @@ async function addWorkspaceComponents(
targetConfig.resolvedPaths.cwd
)) ?? targetConfig.resolvedPaths.cwd
- // Update files for this type.
- const files = await updateFiles(typeFiles, targetConfig, {
+ // Update files for this target config.
+ const files = await updateFiles(targetFiles, targetConfig, {
overwrite: options.overwrite,
silent: true,
rootSpinner,
diff --git a/packages/shadcn/src/utils/updaters/update-files.ts b/packages/shadcn/src/utils/updaters/update-files.ts
index bf1bf7a08..3e9bb95fb 100644
--- a/packages/shadcn/src/utils/updaters/update-files.ts
+++ b/packages/shadcn/src/utils/updaters/update-files.ts
@@ -40,6 +40,9 @@ import { z } from "zod"
const CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"]
const NON_ALIAS_RESOLVED_PATH_KEYS = new Set(["tailwindConfig", "tailwindCss"])
+const TARGET_ALIAS_KEYS = ["components", "ui", "lib", "hooks"] as const
+
+type TargetAliasKey = (typeof TARGET_ALIAS_KEYS)[number]
export async function updateFiles(
files: RegistryItem["files"],
@@ -412,6 +415,15 @@ export function resolveFilePath(
}
let target = file.target
+ const aliasTarget = resolveAliasTarget(target, config)
+
+ if (aliasTarget?.resolvedPath) {
+ return aliasTarget.resolvedPath
+ }
+
+ if (aliasTarget?.target) {
+ target = aliasTarget.target
+ }
if (file.type === "registry:page") {
target = resolvePageTarget(target, options.framework)
@@ -431,6 +443,42 @@ export function resolveFilePath(
return path.join(targetDir, relativePath)
}
+function resolveAliasTarget(target: string, config: Config) {
+ const match = target.match(/^@([^/]+)\/(.+)$/)
+
+ if (!match) {
+ return null
+ }
+
+ const [, aliasKey, targetPath] = match
+
+ if (!isTargetAliasKey(aliasKey)) {
+ return {
+ target: `${aliasKey}/${targetPath}`,
+ }
+ }
+
+ const aliasRoot = path.resolve(config.resolvedPaths[aliasKey])
+ const resolvedPath = path.resolve(aliasRoot, targetPath)
+
+ if (
+ resolvedPath !== aliasRoot &&
+ !resolvedPath.startsWith(`${aliasRoot}${path.sep}`)
+ ) {
+ throw new Error(
+ `Invalid target path "${target}". Target paths using @${aliasKey}/ must stay within the ${aliasKey} alias root.`
+ )
+ }
+
+ return {
+ resolvedPath,
+ }
+}
+
+function isTargetAliasKey(aliasKey: string): aliasKey is TargetAliasKey {
+ return TARGET_ALIAS_KEYS.includes(aliasKey as TargetAliasKey)
+}
+
function resolveFileTargetDirectory(
file: z.infer,
config: Config
diff --git a/packages/shadcn/test/utils/updaters/update-files.test.ts b/packages/shadcn/test/utils/updaters/update-files.test.ts
index dd53690cc..0b51fc548 100644
--- a/packages/shadcn/test/utils/updaters/update-files.test.ts
+++ b/packages/shadcn/test/utils/updaters/update-files.test.ts
@@ -109,6 +109,178 @@ describe("resolveFilePath", () => {
).toBe(resolvedPath)
})
+ test.each([
+ {
+ description: "should resolve @components target aliases",
+ target: "@components/charts/pie.tsx",
+ resolvedPath: "/foo/bar/components/charts/pie.tsx",
+ },
+ {
+ description: "should resolve @ui target aliases",
+ target: "@ui/button.tsx",
+ resolvedPath: "/foo/bar/components/ui/button.tsx",
+ },
+ {
+ description: "should resolve @lib target aliases",
+ target: "@lib/format.ts",
+ resolvedPath: "/foo/bar/lib/format.ts",
+ },
+ {
+ description: "should resolve @hooks target aliases",
+ target: "@hooks/use-theme.ts",
+ resolvedPath: "/foo/bar/hooks/use-theme.ts",
+ },
+ ])("$description", ({ target, resolvedPath }) => {
+ expect(
+ resolveFilePath(
+ {
+ path: "hello-world/ui/button.tsx",
+ type: "registry:ui",
+ target,
+ },
+ {
+ resolvedPaths: {
+ cwd: "/foo/bar",
+ components: "/foo/bar/components",
+ ui: "/foo/bar/components/ui",
+ lib: "/foo/bar/lib",
+ hooks: "/foo/bar/hooks",
+ },
+ },
+ {
+ isSrcDir: true,
+ }
+ )
+ ).toBe(resolvedPath)
+ })
+
+ test("should resolve target aliases with package import backed aliases", () => {
+ expect(
+ resolveFilePath(
+ {
+ path: "hello-world/ui/button.tsx",
+ type: "registry:ui",
+ target: "@ui/button.tsx",
+ },
+ {
+ aliases: {
+ components: "#components",
+ ui: "#components/ui",
+ lib: "#lib",
+ hooks: "#hooks",
+ },
+ resolvedPaths: {
+ cwd: "/foo/bar",
+ components: "/foo/bar/src/components",
+ ui: "/foo/bar/src/components/ui",
+ lib: "/foo/bar/src/lib",
+ hooks: "/foo/bar/src/hooks",
+ },
+ },
+ {
+ isSrcDir: false,
+ }
+ )
+ ).toBe("/foo/bar/src/components/ui/button.tsx")
+ })
+
+ test("should fall back to normal target resolution for unknown aliases", () => {
+ expect(
+ resolveFilePath(
+ {
+ path: "hello-world/ui/button.tsx",
+ type: "registry:ui",
+ target: "@foo/bar.ts",
+ },
+ {
+ resolvedPaths: {
+ cwd: "/foo/bar",
+ components: "/foo/bar/components",
+ ui: "/foo/bar/components/ui",
+ lib: "/foo/bar/lib",
+ hooks: "/foo/bar/hooks",
+ },
+ },
+ {
+ isSrcDir: true,
+ }
+ )
+ ).toBe("/foo/bar/src/foo/bar.ts")
+ })
+
+ test("should not resolve embedded alias-like path segments", () => {
+ expect(
+ resolveFilePath(
+ {
+ path: "hello-world/ui/button.tsx",
+ type: "registry:ui",
+ target: "components/@ui/button.tsx",
+ },
+ {
+ resolvedPaths: {
+ cwd: "/foo/bar",
+ components: "/foo/bar/components",
+ ui: "/foo/bar/components/ui",
+ lib: "/foo/bar/lib",
+ hooks: "/foo/bar/hooks",
+ },
+ },
+ {
+ isSrcDir: false,
+ }
+ )
+ ).toBe("/foo/bar/components/@ui/button.tsx")
+ })
+
+ test("should bypass page target mapping for target aliases", () => {
+ expect(
+ resolveFilePath(
+ {
+ path: "hello-world/app/login/page.tsx",
+ type: "registry:page",
+ target: "@ui/page.tsx",
+ },
+ {
+ resolvedPaths: {
+ cwd: "/foo/bar",
+ components: "/foo/bar/src/components",
+ ui: "/foo/bar/src/primitives",
+ lib: "/foo/bar/src/lib",
+ hooks: "/foo/bar/src/hooks",
+ },
+ },
+ {
+ isSrcDir: true,
+ framework: "next-pages",
+ }
+ )
+ ).toBe("/foo/bar/src/primitives/page.tsx")
+ })
+
+ test("should reject target aliases that escape the alias root", () => {
+ expect(() =>
+ resolveFilePath(
+ {
+ path: "hello-world/ui/button.tsx",
+ type: "registry:ui",
+ target: "@ui/../../page.tsx",
+ },
+ {
+ resolvedPaths: {
+ cwd: "/foo/bar",
+ components: "/foo/bar/components",
+ ui: "/foo/bar/components/ui",
+ lib: "/foo/bar/lib",
+ hooks: "/foo/bar/hooks",
+ },
+ },
+ {
+ isSrcDir: false,
+ }
+ )
+ ).toThrow('Invalid target path "@ui/../../page.tsx".')
+ })
+
test.each([
{
description: "should use src directory when provided",
diff --git a/packages/tests/fixtures/registry/example-target-alias-child.json b/packages/tests/fixtures/registry/example-target-alias-child.json
new file mode 100644
index 000000000..7079d877d
--- /dev/null
+++ b/packages/tests/fixtures/registry/example-target-alias-child.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "example-target-alias-child",
+ "type": "registry:component",
+ "files": [
+ {
+ "path": "registry/example-target-alias-child/ui/dependency-button.tsx",
+ "type": "registry:ui",
+ "content": "export function DependencyButton() {\n return \n}\n",
+ "target": "@ui/dependency-button.tsx"
+ },
+ {
+ "path": "registry/example-target-alias-child/lib/dependency-helper.ts",
+ "type": "registry:lib",
+ "content": "export function dependencyHelper() {\n return \"dependency-helper\"\n}\n",
+ "target": "@lib/dependency-helper.ts"
+ }
+ ]
+}
diff --git a/packages/tests/fixtures/registry/example-target-alias-parent.json b/packages/tests/fixtures/registry/example-target-alias-parent.json
new file mode 100644
index 000000000..494fa21ed
--- /dev/null
+++ b/packages/tests/fixtures/registry/example-target-alias-parent.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "example-target-alias-parent",
+ "type": "registry:component",
+ "registryDependencies": [
+ "../../fixtures/registry/example-target-alias-child.json"
+ ],
+ "files": [
+ {
+ "path": "registry/example-target-alias-parent/components/dependency-panel.tsx",
+ "type": "registry:component",
+ "content": "export function DependencyPanel() {\n return Dependency Panel
\n}\n",
+ "target": "@components/dependency-panel.tsx"
+ },
+ {
+ "path": "registry/example-target-alias-parent/hooks/use-dependency.ts",
+ "type": "registry:hook",
+ "content": "export function useDependency() {\n return \"dependency\"\n}\n",
+ "target": "@hooks/use-dependency.ts"
+ }
+ ]
+}
diff --git a/packages/tests/fixtures/registry/example-target-alias-type-mismatch.json b/packages/tests/fixtures/registry/example-target-alias-type-mismatch.json
new file mode 100644
index 000000000..017586d8e
--- /dev/null
+++ b/packages/tests/fixtures/registry/example-target-alias-type-mismatch.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "example-target-alias-type-mismatch",
+ "type": "registry:component",
+ "files": [
+ {
+ "path": "registry/example-target-alias-type-mismatch/ui/target-from-ui-type.ts",
+ "type": "registry:ui",
+ "content": "export function targetFromUiType() {\n return \"target-from-ui-type\"\n}\n",
+ "target": "@lib/target-from-ui-type.ts"
+ }
+ ]
+}
diff --git a/packages/tests/fixtures/registry/example-target-aliases.json b/packages/tests/fixtures/registry/example-target-aliases.json
new file mode 100644
index 000000000..ff09a584b
--- /dev/null
+++ b/packages/tests/fixtures/registry/example-target-aliases.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "example-target-aliases",
+ "type": "registry:component",
+ "files": [
+ {
+ "path": "registry/example-target-aliases/ui/target-button.tsx",
+ "type": "registry:ui",
+ "content": "export function TargetButton() {\n return \n}\n",
+ "target": "@ui/target-button.tsx"
+ },
+ {
+ "path": "registry/example-target-aliases/components/target-panel.tsx",
+ "type": "registry:component",
+ "content": "export function TargetPanel() {\n return Target Panel
\n}\n",
+ "target": "@components/target-panel.tsx"
+ },
+ {
+ "path": "registry/example-target-aliases/lib/target-helper.ts",
+ "type": "registry:lib",
+ "content": "export function targetHelper() {\n return \"target-helper\"\n}\n",
+ "target": "@lib/target-helper.ts"
+ },
+ {
+ "path": "registry/example-target-aliases/hooks/use-target.ts",
+ "type": "registry:hook",
+ "content": "export function useTarget() {\n return \"target\"\n}\n",
+ "target": "@hooks/use-target.ts"
+ }
+ ]
+}
diff --git a/packages/tests/src/tests/add.test.ts b/packages/tests/src/tests/add.test.ts
index c811cda34..ee9671733 100644
--- a/packages/tests/src/tests/add.test.ts
+++ b/packages/tests/src/tests/add.test.ts
@@ -258,6 +258,74 @@ describe("shadcn add", () => {
})
})
+ it("should add item with registry target aliases", async () => {
+ const fixturePath = await createFixtureTestDirectory("next-app-init")
+ const result = await npxShadcn(fixturePath, [
+ "add",
+ "../../fixtures/registry/example-target-aliases.json",
+ ])
+
+ expectCommandSuccess(result)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "components/ui/target-button.tsx")
+ )
+ ).toBe(true)
+ expect(
+ await fs.pathExists(path.join(fixturePath, "components/target-panel.tsx"))
+ ).toBe(true)
+ expect(
+ await fs.pathExists(path.join(fixturePath, "lib/target-helper.ts"))
+ ).toBe(true)
+ expect(
+ await fs.pathExists(path.join(fixturePath, "hooks/use-target.ts"))
+ ).toBe(true)
+ })
+
+ it("should add registryDependencies with registry target aliases", async () => {
+ const fixturePath = await createFixtureTestDirectory("next-app-init")
+ const result = await npxShadcn(fixturePath, [
+ "add",
+ "../../fixtures/registry/example-target-alias-parent.json",
+ ])
+
+ expectCommandSuccess(result)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "components/ui/dependency-button.tsx")
+ )
+ ).toBe(true)
+ expect(
+ await fs.pathExists(path.join(fixturePath, "lib/dependency-helper.ts"))
+ ).toBe(true)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "components/dependency-panel.tsx")
+ )
+ ).toBe(true)
+ expect(
+ await fs.pathExists(path.join(fixturePath, "hooks/use-dependency.ts"))
+ ).toBe(true)
+ })
+
+ it("should prefer registry target aliases over the file type", async () => {
+ const fixturePath = await createFixtureTestDirectory("next-app-init")
+ const result = await npxShadcn(fixturePath, [
+ "add",
+ "../../fixtures/registry/example-target-alias-type-mismatch.json",
+ ])
+
+ expectCommandSuccess(result)
+ expect(
+ await fs.pathExists(path.join(fixturePath, "lib/target-from-ui-type.ts"))
+ ).toBe(true)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "components/ui/target-from-ui-type.ts")
+ )
+ ).toBe(false)
+ })
+
it("should add item with envVars", async () => {
const fixturePath = await createFixtureTestDirectory("next-app")
await npxShadcn(fixturePath, ["init", "--defaults"])
@@ -324,6 +392,89 @@ describe("shadcn add", () => {
expect(buttonContent).toContain('import { cn } from "#lib/utils.ts"')
}, 300000)
+ it("should add monorepo item with registry target aliases and package imports", async () => {
+ const fixturePath = await createFixtureTestDirectory(
+ "vite-monorepo-imports"
+ )
+
+ const result = await npxShadcn(
+ fixturePath,
+ [
+ "add",
+ "../../fixtures/registry/example-target-aliases.json",
+ "-c",
+ "apps/web",
+ "--yes",
+ ],
+ { timeout: 300000 }
+ )
+
+ expectCommandSuccess(result)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "packages/ui/src/components/target-button.tsx")
+ )
+ ).toBe(true)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "apps/web/src/components/target-panel.tsx")
+ )
+ ).toBe(true)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "apps/web/src/lib/target-helper.ts")
+ )
+ ).toBe(true)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "apps/web/src/hooks/use-target.ts")
+ )
+ ).toBe(true)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "apps/web/src/components/ui/target-button.tsx")
+ )
+ ).toBe(false)
+ }, 300000)
+
+ it("should prefer registry target aliases over the file type in monorepos", async () => {
+ const fixturePath = await createFixtureTestDirectory(
+ "vite-monorepo-imports"
+ )
+
+ const result = await npxShadcn(
+ fixturePath,
+ [
+ "add",
+ "../../fixtures/registry/example-target-alias-type-mismatch.json",
+ "-c",
+ "apps/web",
+ "--yes",
+ ],
+ { timeout: 300000 }
+ )
+
+ expectCommandSuccess(result)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "apps/web/src/lib/target-from-ui-type.ts")
+ )
+ ).toBe(true)
+ expect(
+ await fs.pathExists(
+ path.join(fixturePath, "packages/ui/src/lib/target-from-ui-type.ts")
+ )
+ ).toBe(false)
+ expect(
+ await fs.pathExists(
+ path.join(
+ fixturePath,
+ "packages/ui/src/components/target-from-ui-type.ts"
+ )
+ )
+ ).toBe(false)
+ }, 300000)
+
it("should preview monorepo adds without writing files", async () => {
const fixturePath = await createFixtureTestDirectory(
"vite-monorepo-imports"