mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-01 08:34:12 +00:00
* feat: add support for package imports * fix * test(cli): surface add command failures * test(cli): remove stale pnpm pin from fixture * fix(cli): reject invalid package import targets * fix(cli): address package import review feedback * test: expand coverage * docs: add package imports docs
671 lines
16 KiB
TypeScript
671 lines
16 KiB
TypeScript
import { expect, test } from "vitest"
|
|
|
|
import { transform } from "../../src/utils/transformers"
|
|
|
|
test("transform nested workspace folder for utils, website/src/utils", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
|
|
raw: `import { Button } from "website/src/components/ui/button"
|
|
import { Box } from "website/src/components/box"
|
|
import { cn } from "website/src/utils"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
tailwind: {
|
|
baseColor: "neutral",
|
|
cssVariables: true,
|
|
},
|
|
aliases: {
|
|
components: "website/src/components",
|
|
lib: "website/src/lib",
|
|
utils: "website/src/utils",
|
|
},
|
|
},
|
|
})
|
|
).toMatchInlineSnapshot(`
|
|
"import { Button } from "website/src/components/ui/button"
|
|
import { Box } from "website/src/components/box"
|
|
import { cn } from "website/src/utils"
|
|
"
|
|
`)
|
|
})
|
|
|
|
test.each([
|
|
{
|
|
name: "bare aliases",
|
|
aliases: {
|
|
components: "components",
|
|
ui: "components/ui",
|
|
lib: "lib",
|
|
utils: "lib/utils",
|
|
},
|
|
buttonImport: `import { Button } from "components/ui/button"`,
|
|
utilsImport: `import { cn } from "lib/utils"`,
|
|
},
|
|
{
|
|
name: "path-like aliases",
|
|
aliases: {
|
|
components: "website/src/components",
|
|
ui: "website/src/components/ui",
|
|
lib: "website/src/lib",
|
|
utils: "website/src/lib/utils",
|
|
},
|
|
buttonImport: `import { Button } from "website/src/components/ui/button"`,
|
|
utilsImport: `import { cn } from "website/src/lib/utils"`,
|
|
},
|
|
])(
|
|
"transform import with non-sigil aliases: $name",
|
|
async ({ aliases, buttonImport, utilsImport }) => {
|
|
const result = await transform({
|
|
filename: "test.ts",
|
|
raw: `import { Button } from "@/registry/new-york/ui/button"
|
|
import { cn } from "@/lib/utils"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases,
|
|
},
|
|
})
|
|
|
|
expect(result).toContain(buttonImport)
|
|
expect(result).toContain(utilsImport)
|
|
}
|
|
)
|
|
|
|
test("transform import", async () => {
|
|
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"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
tailwind: {
|
|
baseColor: "neutral",
|
|
cssVariables: true,
|
|
},
|
|
aliases: {
|
|
components: "@/components",
|
|
utils: "@/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).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, foo, bar } from "@/lib/utils"
|
|
import { bar } from "@/lib/utils/bar"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "~/src/components",
|
|
utils: "~/lib",
|
|
},
|
|
},
|
|
})
|
|
).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",
|
|
},
|
|
},
|
|
})
|
|
).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()
|
|
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import * as React from "react"
|
|
import { Foo } from "bar"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Label} from "ui/label"
|
|
import { Box } from "@/registry/new-york/box"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
tailwind: {
|
|
baseColor: "neutral",
|
|
cssVariables: true,
|
|
},
|
|
aliases: {
|
|
components: "@custom-alias/components",
|
|
utils: "@custom-alias/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
test("transform import with configured package-import aliases", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import { Button } from "#app/components/ui/button"
|
|
import { cn } from "#app/lib/utils"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#app/components",
|
|
ui: "#app/components/ui",
|
|
lib: "#app/lib",
|
|
utils: "#app/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toMatchInlineSnapshot(`
|
|
"import { Button } from "#app/components/ui/button"
|
|
import { cn } from "#app/lib/utils"
|
|
"
|
|
`)
|
|
})
|
|
|
|
test("transform import keeps exact #utils aliases", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import { cn } from "@/lib/utils"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
utils: "#utils",
|
|
ui: "#components/ui",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
},
|
|
},
|
|
})
|
|
).toMatchInlineSnapshot(`
|
|
"import { cn } from "#utils"
|
|
"
|
|
`)
|
|
})
|
|
|
|
test("transform import keeps #lib/utils aliases", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import { cn } from "@/lib/utils"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
utils: "#lib/utils",
|
|
ui: "#components/ui",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
},
|
|
},
|
|
})
|
|
).toMatchInlineSnapshot(`
|
|
"import { cn } from "#lib/utils"
|
|
"
|
|
`)
|
|
})
|
|
|
|
test("transform import for monorepo", async () => {
|
|
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"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
tailwind: {
|
|
baseColor: "neutral",
|
|
cssVariables: true,
|
|
},
|
|
aliases: {
|
|
components: "@workspace/ui/components",
|
|
utils: "@workspace/ui/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).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"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
tailwind: {
|
|
baseColor: "neutral",
|
|
cssVariables: true,
|
|
},
|
|
aliases: {
|
|
components: "@repo/ui/components",
|
|
utils: "@repo/ui/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
test("transform package import aliases and #registry placeholders", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import { Button } from "#registry/new-york/ui/button"
|
|
import { Card } from "#/registry/new-york/ui/card"
|
|
import * as RegistryRoot from "#registry"
|
|
import * as RegistryRootCompat from "#/registry"
|
|
import { cn } from "#utils"
|
|
import { helper } from "#lib/helpers"
|
|
import { useThing } from "#hooks/use-thing"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
ui: "#components/ui",
|
|
utils: "#utils",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
},
|
|
},
|
|
})
|
|
).toContain(`import { Button } from "#components/ui/button"`)
|
|
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import { Button } from "#registry/new-york/ui/button"
|
|
import { Card } from "#/registry/new-york/ui/card"
|
|
import * as RegistryRoot from "#registry"
|
|
import * as RegistryRootCompat from "#/registry"
|
|
import { cn } from "#utils"
|
|
import { helper } from "#lib/helpers"
|
|
import { useThing } from "#hooks/use-thing"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
ui: "#components/ui",
|
|
utils: "#utils",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
},
|
|
},
|
|
})
|
|
).toContain(`import { Card } from "#components/ui/card"`)
|
|
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import { Button } from "#registry/new-york/ui/button"
|
|
import { Card } from "#/registry/new-york/ui/card"
|
|
import * as RegistryRoot from "#registry"
|
|
import * as RegistryRootCompat from "#/registry"
|
|
import { cn } from "#utils"
|
|
import { helper } from "#lib/helpers"
|
|
import { useThing } from "#hooks/use-thing"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
ui: "#components/ui",
|
|
utils: "#utils",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
},
|
|
},
|
|
})
|
|
).toContain(`import { cn } from "#utils"`)
|
|
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import * as RegistryRoot from "#registry"
|
|
import * as RegistryRootCompat from "#/registry"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
ui: "#components/ui",
|
|
utils: "#utils",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
},
|
|
},
|
|
})
|
|
).toContain(`import * as RegistryRoot from "#components"`)
|
|
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import * as RegistryRoot from "#registry"
|
|
import * as RegistryRootCompat from "#/registry"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
ui: "#components/ui",
|
|
utils: "#utils",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
},
|
|
},
|
|
})
|
|
).toContain(`import * as RegistryRootCompat from "#components"`)
|
|
})
|
|
|
|
test("prefers explicit workspace utils alias over local lib alias", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "test.tsx",
|
|
raw: `import { cn } from "@/lib/utils"
|
|
import { helper } from "@/lib/helper"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
ui: "@workspace/ui/components",
|
|
utils: "@workspace/ui/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toContain(`import { cn } from "@workspace/ui/lib/utils"`)
|
|
})
|
|
|
|
test("prefers explicit utils alias for registry lib utils imports", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "login-form.tsx",
|
|
raw: `import { cn } from "@/registry/new-york-v4/lib/utils"
|
|
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
ui: "@workspace/ui/components",
|
|
utils: "@workspace/ui/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toContain(`import { cn } from "@workspace/ui/lib/utils"`)
|
|
})
|
|
|
|
test("transform async/dynamic imports", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import * as React from "react"
|
|
import { Button } from "@/registry/new-york/ui/button"
|
|
|
|
async function loadComponent() {
|
|
const { cn } = await import("@/lib/utils")
|
|
const module = await import("@/registry/new-york/ui/card")
|
|
return module
|
|
}
|
|
|
|
function lazyLoad() {
|
|
return import("@/registry/new-york/ui/dialog").then(module => module)
|
|
}
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "@/components",
|
|
utils: "@/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toMatchSnapshot()
|
|
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `import { Button } from "@/registry/new-york/ui/button"
|
|
|
|
async function loadUtils() {
|
|
const utils = await import("@/lib/utils")
|
|
const { cn } = await import("@/lib/utils")
|
|
return { utils, cn }
|
|
}
|
|
|
|
const dialogPromise = import("@/registry/new-york/ui/dialog")
|
|
const cardModule = import("@/registry/new-york/ui/card")
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "~/components",
|
|
utils: "~/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
test("transform dynamic imports with cn utility", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `async function loadCn() {
|
|
const { cn } = await import("@/lib/utils")
|
|
return cn
|
|
}
|
|
|
|
async function loadMultiple() {
|
|
const utils1 = await import("@/lib/utils")
|
|
const { cn, twMerge } = await import("@/lib/utils")
|
|
const other = await import("@/lib/other")
|
|
}
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "@/components",
|
|
utils: "@/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toMatchSnapshot()
|
|
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `async function loadWorkspaceCn() {
|
|
const { cn } = await import("@/lib/utils")
|
|
return cn
|
|
}
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "@workspace/ui/components",
|
|
utils: "@workspace/ui/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toMatchSnapshot()
|
|
})
|
|
|
|
test("does not rewrite foreign scoped package imports when project uses # aliases", async () => {
|
|
const result = await transform({
|
|
filename: "test.tsx",
|
|
raw: `import { Analytics } from "@vercel/analytics/react"
|
|
import posthog from "posthog-js"
|
|
import { motion } from "motion/react"
|
|
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
ui: "#components/ui",
|
|
utils: "#utils",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(result).toContain(
|
|
`import { Analytics } from "@vercel/analytics/react"`
|
|
)
|
|
expect(result).toContain(`import posthog from "posthog-js"`)
|
|
expect(result).toContain(`import { motion } from "motion/react"`)
|
|
expect(result).toContain(`import { Button } from "#components/ui/button"`)
|
|
})
|
|
|
|
test("does not rewrite workspace package exports when project uses # aliases", async () => {
|
|
const result = await transform({
|
|
filename: "test.tsx",
|
|
raw: `import { Card } from "@workspace/ui/components/card"
|
|
import { useTheme } from "@workspace/ui/hooks/use-theme"
|
|
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "#components",
|
|
ui: "@workspace/ui/components",
|
|
utils: "@workspace/ui/lib/utils",
|
|
lib: "#lib",
|
|
hooks: "#hooks",
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(result).toContain(
|
|
`import { Card } from "@workspace/ui/components/card"`
|
|
)
|
|
expect(result).toContain(
|
|
`import { useTheme } from "@workspace/ui/hooks/use-theme"`
|
|
)
|
|
expect(result).toContain(
|
|
`import { Button } from "@workspace/ui/components/button"`
|
|
)
|
|
})
|
|
|
|
test("transform re-exports with dynamic imports", async () => {
|
|
expect(
|
|
await transform({
|
|
filename: "test.ts",
|
|
raw: `export { cn } from "@/lib/utils"
|
|
export { Button } from "@/registry/new-york/ui/button"
|
|
|
|
async function load() {
|
|
const module = await import("@/registry/new-york/ui/card")
|
|
return module
|
|
}
|
|
`,
|
|
config: {
|
|
tsx: true,
|
|
aliases: {
|
|
components: "@/components",
|
|
utils: "@/lib/utils",
|
|
},
|
|
},
|
|
})
|
|
).toMatchSnapshot()
|
|
})
|