Compare commits

...

16 Commits

Author SHA1 Message Date
shadcn
0826d58d3a chore: ignore errors 2025-05-29 13:36:19 +04:00
shadcn
995b6fd1a8 fix: format 2025-05-29 12:59:39 +04:00
shadcn
410998525b fix: to v3 2025-05-29 12:47:58 +04:00
shadcn
0f18c2775a Merge branch 'main' into v2 2025-05-29 12:45:59 +04:00
Thibault Le Ouay
b84c990e42 fix registry item schema (#7486) 2025-05-28 16:52:24 +04:00
Joshua
2773f9e2e2 fix(tailwind-prefix): resolve prefixing issue for Tailwind CSS v4 compatibility (including tests) (#6885)
* WIP

* fix(tailwind-prefix): resolve prefixing issue for Tailwind CSS v4 compatibility
1. Fixed incorrect prefix application causing issues with Tailwind CSS v4.
2. Optimized implementation using map() for better performance and readability.

* fix(tailwind-prefix): fix test transform-tw-prefixt

* fix(tailwind-prefix): fix test apply-prefix

* fix(tailwind-prefix): add backwards compatibility for applyPrefix

* fix(tailwind-prefix): added changeset

---------

Co-authored-by: Al-Amin Islam Nerob <nerobit.786@gmail.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-05-19 11:31:12 +04:00
shadcn
c41c6ece86 fix: move tw-animate-css to devDependencies (#7251) 2025-04-23 16:45:23 +04:00
github-actions[bot]
11267f2fed chore(release): version packages (#7228)
* chore(release): version packages

* deps: update pnpm lock

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-04-23 16:28:59 +04:00
Richard Szalay
9ad24d6a16 chore(shadcn): add update-dependencies tests (#7067) 2025-04-22 15:20:00 +04:00
shadcn
e8468793fc chore: temporarily move tw-animate-css to dependencies 2025-04-22 05:07:56 +04:00
迷渡
6f702f5fbf fix: Add npm: specifier when install dependencies with Deno (#6899)
* fix: Add `npm:` specifier when install dependencies with Deno

* chore: changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-04-21 23:36:36 +04:00
shadcn
d0306774fe feat(shadcn): resolve imports from anywhere (#7220)
* feat(shadcn): resolve imports from anywhere

* fix: type errors

* fix: add debug

* feat: handle root paths

* fix: src prefix

* fix: tests

* chore: changeset
2025-04-19 13:31:04 +04:00
Neeraj Dalal
f1e5cc4666 🔥 feat(fix): mark "tw-animate-css" as devDep (#6985)
* chore: tweaks to build-registry.mts ~ 1 file 2+ 5-

apps/v4/scripts/build-registry.mts

* chore: tweaks to index.json ~ 1 file 4+ 9-

apps/www/public/r/styles/new-york-v4/index.json

* chore: tweaks to registry.json ~ 1 file 221+ 850-

apps/v4/registry.json

* chore: tweaks to index.json ~ 1 file 47+ 144-

apps/www/public/r/index.json

* chore: tweaks to registry.json ~ 1 file 852+ 221-

apps/v4/registry.json

* chore: tweaks to build-registry.mts ~ 1 file 7+ 2-

apps/v4/scripts/build-registry.mts

* chore: tweaks to build-registry.mts ~ 1 file 2+ 2-

apps/v4/scripts/build-registry.mts

* chore: tweaks to index.json ~ 1 file 11+ 4-

apps/www/public/r/styles/new-york-v4/index.json

* chore: tweaks to index.json ~ 1 file 1+ 1-

apps/www/public/r/index.json

* chore: tweaks to index.json ~ 1 file 143+ 46-

apps/www/public/r/index.json

* chore: tweaks to update-dependencies.ts ~ 1 file 20+

packages/shadcn/src/utils/updaters/update-dependencies.ts

* chore: tweaks to update-dependencies.ts ~ 1 file 1-

packages/shadcn/src/utils/updaters/update-dependencies.ts

* chore: tweaks to update-dependencies.ts ~ 1 file 19-

packages/shadcn/src/utils/updaters/update-dependencies.ts

* chore: tweaks to add-components.ts update-dependencies.ts ~ 2 files 21+ 2-

packages/shadcn/src/utils/add-components.ts
packages/shadcn/src/utils/updaters/update-dependencies.ts

* chore: tweaks to update-dependencies.ts ~ 1 file 29+ 28-

packages/shadcn/src/utils/updaters/update-dependencies.ts

* refactor: remove redundant code

tw-animate-css already has accordion animations

* chore: tweaks > migrate-icons.ts

packages/shadcn/src/migrations/migrate-icons.ts

* fix: formatting > add-components.ts update-dependencies.ts

packages/shadcn/src/utils/add-components.ts
packages/shadcn/src/utils/updaters/update-dependencies.ts

* chore: add changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-04-19 13:05:52 +04:00
Titouan V
e3ca257f6e chore(www): replace shadcn-ui deprecated mentions to shadcn (#7207) 2025-04-18 10:44:46 +04:00
shadcn
a6034127f9 fix: lint 2025-03-28 11:40:06 +04:00
shadcn
eaf8156fc0 chore: add banner 2025-03-28 11:31:48 +04:00
37 changed files with 985 additions and 157 deletions

View File

@@ -0,0 +1,5 @@
---
"shadcn": patch
---
resolved prefixing issue for Tailwind CSS v4 compatibility

View File

@@ -2,11 +2,11 @@
import * as React from "react"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
closestCenter,
useSensor,
useSensors,
type DragEndEvent,
@@ -14,8 +14,8 @@ import {
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
SortableContext,
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
@@ -37,9 +37,6 @@ import {
import {
ColumnDef,
ColumnFiltersState,
Row,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
@@ -47,7 +44,10 @@ import {
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { toast } from "sonner"

View File

@@ -8,6 +8,7 @@ import { Toaster } from "@/registry/new-york-v4/ui/sonner"
import { siteConfig } from "@/www/config/site"
import "./globals.css"
import { cn } from "@/lib/utils"
import { ActiveThemeProvider } from "@/components/active-theme"

View File

@@ -1,8 +1,8 @@
"use client"
import {
ReactNode,
createContext,
ReactNode,
useContext,
useEffect,
useState,

View File

@@ -73,7 +73,7 @@
"react-resizable-panels": "^2.1.7",
"recharts": "2.15.1",
"rimraf": "^6.0.1",
"shadcn": "2.4.1",
"shadcn": "2.5.0",
"sonner": "^2.0.0",
"tailwind-merge": "^3.0.1",
"tailwindcss": "^4.0.7",

View File

@@ -6,10 +6,12 @@
"name": "index",
"type": "registry:style",
"dependencies": [
"tw-animate-css",
"class-variance-authority",
"lucide-react"
],
"devDependencies": [
"tw-animate-css"
],
"registryDependencies": [
"utils"
],
@@ -27,37 +29,7 @@
"path": "registry/new-york-v4/ui/accordion.tsx",
"type": "registry:ui"
}
],
"tailwind": {
"config": {
"theme": {
"extend": {
"keyframes": {
"accordion-down": {
"from": {
"height": "0"
},
"to": {
"height": "var(--radix-accordion-content-height)"
}
},
"accordion-up": {
"from": {
"height": "var(--radix-accordion-content-height)"
},
"to": {
"height": "0"
}
}
},
"animation": {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out"
}
}
}
}
}
]
},
{
"name": "alert",

View File

@@ -2,11 +2,11 @@
import * as React from "react"
import {
closestCenter,
DndContext,
KeyboardSensor,
MouseSensor,
TouchSensor,
closestCenter,
useSensor,
useSensors,
type DragEndEvent,
@@ -14,8 +14,8 @@ import {
} from "@dnd-kit/core"
import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
import {
SortableContext,
arrayMove,
SortableContext,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable"
@@ -37,9 +37,6 @@ import {
import {
ColumnDef,
ColumnFiltersState,
Row,
SortingState,
VisibilityState,
flexRender,
getCoreRowModel,
getFacetedRowModel,
@@ -47,7 +44,10 @@ import {
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
Row,
SortingState,
useReactTable,
VisibilityState,
} from "@tanstack/react-table"
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"
import { toast } from "sonner"

View File

@@ -39,7 +39,7 @@ function ResizableHandle({
<ResizablePrimitive.PanelResizeHandle
data-slot="resizable-handle"
className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90",
className
)}
{...props}

View File

@@ -2,7 +2,7 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority"
import { cva, VariantProps } from "class-variance-authority"
import { PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@/registry/new-york-v4/hooks/use-mobile"

View File

@@ -20,11 +20,8 @@ const registry = {
{
name: "index",
type: "registry:style",
dependencies: [
"tw-animate-css",
"class-variance-authority",
"lucide-react",
],
dependencies: ["class-variance-authority", "lucide-react"],
devDependencies: ["tw-animate-css"],
registryDependencies: ["utils"],
cssVars: {},
files: [],
@@ -78,6 +75,11 @@ const registry = {
if (item.name === "dashboard-01") {
item.dependencies?.push("@tabler/icons-react")
}
if (item.name === "accordion" && "tailwind" in item) {
delete item.tailwind
}
return item
})
),

View File

@@ -1,5 +1,7 @@
import "@/styles/globals.css"
import { Metadata, Viewport } from "next"
import Link from "next/link"
import { ArrowRightIcon } from "lucide-react"
import { META_THEME_COLORS, siteConfig } from "@/config/site"
import { fontMono, fontSans } from "@/lib/fonts"
@@ -91,7 +93,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
</head>
<body
className={cn(
"min-h-svh bg-background font-sans antialiased",
"bg-background min-h-svh font-sans antialiased",
fontSans.variable,
fontMono.variable
)}
@@ -104,7 +106,16 @@ export default function RootLayout({ children }: RootLayoutProps) {
enableColorScheme
>
<div vaul-drawer-wrapper="">
<div className="relative flex min-h-svh flex-col bg-background">
<div className="bg-background relative flex min-h-svh flex-col">
<div className="bg-muted text-muted-foreground sticky top-0 z-[100] flex h-10 items-center justify-center gap-2 px-4 text-sm">
You are viewing the v3 docs.{" "}
<Link
href="https://ui.shadcn.com"
className="text-primary flex items-center gap-1 underline"
>
Switch to latest <ArrowRightIcon className="size-3" />
</Link>
</div>
{children}
</div>
</div>

View File

@@ -321,7 +321,7 @@ I've been working on a new CLI for the past few weeks. It's a complete rewrite.
### `init`
```bash
npx shadcn-ui@latest init
npx shadcn@latest init
```
When you run the `init` command, you will be asked a few questions to configure `components.json`:
@@ -363,7 +363,7 @@ This means you can now use the CLI with any directory structure including `src`
### `add`
```bash
npx shadcn-ui@latest add
npx shadcn@latest add
```
The `add` command is now much more capable. You can now add UI components but also import more complex components (coming soon).
@@ -373,7 +373,7 @@ The CLI will automatically resolve all components and dependencies, format them
### `diff` (experimental)
```bash
npx shadcn-ui diff
npx shadcn diff
```
We're also introducing a new `diff` command to help you keep track of upstream updates.
@@ -383,7 +383,7 @@ You can use this command to see what has changed in the upstream repository and
Run the `diff` command to get a list of components that have updates available:
```bash
npx shadcn-ui diff
npx shadcn diff
```
```txt
@@ -398,7 +398,7 @@ The following components have updates available:
Then run `diff [component]` to see the changes:
```bash
npx shadcn-ui diff alert
npx shadcn diff alert
```
```diff /pl-12/

View File

@@ -79,7 +79,7 @@ export const onCreateWebpackConfig = ({ actions }) => {
### Run the CLI
Run the `shadcn-ui` init command to setup your project:
Run the `shadcn` init command to setup your project:
```bash
npx shadcn@latest init

View File

@@ -13,7 +13,7 @@ npx create-react-router@latest my-app
### Run the CLI
Run the `shadcn-ui` init command to setup your project:
Run the `shadcn` init command to setup your project:
```bash
npx shadcn@latest init

View File

@@ -26,7 +26,7 @@ npx create-remix@latest my-app
### Run the CLI
Run the `shadcn-ui` init command to setup your project:
Run the `shadcn` init command to setup your project:
```bash
npx shadcn@latest init

View File

@@ -2,6 +2,12 @@ import { createContentlayerPlugin } from "next-contentlayer2"
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: {
ignoreDuringBuilds: true,
},
typescript: {
ignoreBuildErrors: true,
},
experimental: {
outputFileTracingIncludes: {
"/blocks/*": ["./registry/**/*"],

View File

@@ -86,7 +86,7 @@
"react-resizable-panels": "^2.0.22",
"react-wrap-balancer": "^0.4.1",
"recharts": "2.12.7",
"shadcn": "2.4.1",
"shadcn": "2.5.0",
"sharp": "^0.32.6",
"sonner": "^1.2.3",
"swr": "2.2.6-beta.3",

View File

@@ -11,35 +11,5 @@
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { ChevronDownIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Accordion({\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Root>) {\n return <AccordionPrimitive.Root data-slot=\"accordion\" {...props} />\n}\n\nfunction AccordionItem({\n className,\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Item>) {\n return (\n <AccordionPrimitive.Item\n data-slot=\"accordion-item\"\n className={cn(\"border-b last:border-b-0\", className)}\n {...props}\n />\n )\n}\n\nfunction AccordionTrigger({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {\n return (\n <AccordionPrimitive.Header className=\"flex\">\n <AccordionPrimitive.Trigger\n data-slot=\"accordion-trigger\"\n className={cn(\n \"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180\",\n className\n )}\n {...props}\n >\n {children}\n <ChevronDownIcon className=\"text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200\" />\n </AccordionPrimitive.Trigger>\n </AccordionPrimitive.Header>\n )\n}\n\nfunction AccordionContent({\n className,\n children,\n ...props\n}: React.ComponentProps<typeof AccordionPrimitive.Content>) {\n return (\n <AccordionPrimitive.Content\n data-slot=\"accordion-content\"\n className=\"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm\"\n {...props}\n >\n <div className={cn(\"pt-0 pb-4\", className)}>{children}</div>\n </AccordionPrimitive.Content>\n )\n}\n\nexport { Accordion, AccordionItem, AccordionTrigger, AccordionContent }\n",
"type": "registry:ui"
}
],
"tailwind": {
"config": {
"theme": {
"extend": {
"keyframes": {
"accordion-down": {
"from": {
"height": "0"
},
"to": {
"height": "var(--radix-accordion-content-height)"
}
},
"accordion-up": {
"from": {
"height": "var(--radix-accordion-content-height)"
},
"to": {
"height": "0"
}
}
},
"animation": {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out"
}
}
}
}
}
]
}

View File

@@ -3,10 +3,12 @@
"name": "index",
"type": "registry:style",
"dependencies": [
"tw-animate-css",
"class-variance-authority",
"lucide-react"
],
"devDependencies": [
"tw-animate-css"
],
"registryDependencies": [
"utils"
],

View File

@@ -78,7 +78,8 @@
"registry:hook",
"registry:theme",
"registry:page",
"registry:file"
"registry:file",
"registry:style"
],
"description": "The type of the file. This is used to determine the type of the file when resolved for a project."
},

View File

@@ -1,5 +1,17 @@
# @shadcn/ui
## 2.5.0
### Minor Changes
- [#7220](https://github.com/shadcn-ui/ui/pull/7220) [`d0306774fe0ecc1eae9ef1e918bf7862e866a9e8`](https://github.com/shadcn-ui/ui/commit/d0306774fe0ecc1eae9ef1e918bf7862e866a9e8) Thanks [@shadcn](https://github.com/shadcn)! - resolve imports from anywhere
### Patch Changes
- [#6985](https://github.com/shadcn-ui/ui/pull/6985) [`f1e5cc4666ced2166a859660d769ccee16cde46e`](https://github.com/shadcn-ui/ui/commit/f1e5cc4666ced2166a859660d769ccee16cde46e) Thanks [@nrjdalal](https://github.com/nrjdalal)! - move tw-animate-css to devDependencies
- [#6899](https://github.com/shadcn-ui/ui/pull/6899) [`6f702f5fbf2b82a388e7da6ea08bcc84c2ec19c6`](https://github.com/shadcn-ui/ui/commit/6f702f5fbf2b82a388e7da6ea08bcc84c2ec19c6) Thanks [@justjavac](https://github.com/justjavac)! - add deno support
## 2.4.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "shadcn",
"version": "2.4.1",
"version": "2.5.0",
"description": "Add components to your apps.",
"publishConfig": {
"access": "public"

View File

@@ -98,7 +98,7 @@ export async function migrateIcons(config: Config) {
}
if (targetLibrary.package) {
await updateDependencies([targetLibrary.package], config, {
await updateDependencies([targetLibrary.package], [], config, {
silent: false,
})
}

View File

@@ -103,7 +103,7 @@ async function addProjectComponents(
silent: options.silent,
})
await updateDependencies(tree.dependencies, config, {
await updateDependencies(tree.dependencies, tree.devDependencies, config, {
silent: options.silent,
})
await updateFiles(tree.files, config, {
@@ -213,9 +213,14 @@ async function addWorkspaceComponents(
}
// 4. Update dependencies.
await updateDependencies(component.dependencies, targetConfig, {
silent: true,
})
await updateDependencies(
component.dependencies,
component.devDependencies,
targetConfig,
{
silent: true,
}
)
// 5. Update files.
const files = await updateFiles(component.files, targetConfig, {

View File

@@ -1,6 +1,10 @@
import { Transformer } from "@/src/utils/transformers"
import { SyntaxKind } from "ts-morph"
import {
TailwindVersion,
getProjectTailwindVersionFromConfig,
} from "../get-project-info"
import { splitClassName } from "./transform-css-vars"
export const transformTwPrefixes: Transformer = async ({
@@ -10,6 +14,7 @@ export const transformTwPrefixes: Transformer = async ({
if (!config.tailwind?.prefix) {
return sourceFile
}
const tailwindVersion = await getProjectTailwindVersionFromConfig(config)
// Find the cva function calls.
sourceFile
@@ -23,7 +28,8 @@ export const transformTwPrefixes: Transformer = async ({
defaultClassNames.replaceWithText(
`"${applyPrefix(
defaultClassNames.getText()?.replace(/"|'/g, ""),
config.tailwind.prefix
config.tailwind.prefix,
tailwindVersion
)}"`
)
}
@@ -47,7 +53,8 @@ export const transformTwPrefixes: Transformer = async ({
classNames?.replaceWithText(
`"${applyPrefix(
classNames.getText()?.replace(/"|'/g, ""),
config.tailwind.prefix
config.tailwind.prefix,
tailwindVersion
)}"`
)
}
@@ -66,7 +73,8 @@ export const transformTwPrefixes: Transformer = async ({
value.replaceWithText(
`"${applyPrefix(
value.getText()?.replace(/"|'/g, ""),
config.tailwind.prefix
config.tailwind.prefix,
tailwindVersion
)}"`
)
}
@@ -92,7 +100,8 @@ export const transformTwPrefixes: Transformer = async ({
node.replaceWithText(
`"${applyPrefix(
node.getText()?.replace(/"|'/g, ""),
config.tailwind.prefix
config.tailwind.prefix,
tailwindVersion
)}"`
)
})
@@ -102,7 +111,8 @@ export const transformTwPrefixes: Transformer = async ({
node.replaceWithText(
`"${applyPrefix(
node.getText()?.replace(/"|'/g, ""),
config.tailwind.prefix
config.tailwind.prefix,
tailwindVersion
)}"`
)
}
@@ -131,7 +141,8 @@ export const transformTwPrefixes: Transformer = async ({
node.replaceWithText(
`"${applyPrefix(
node.getText()?.replace(/"|'/g, ""),
config.tailwind.prefix
config.tailwind.prefix,
tailwindVersion
)}"`
)
})
@@ -141,7 +152,8 @@ export const transformTwPrefixes: Transformer = async ({
arg.replaceWithText(
`"${applyPrefix(
arg.getText()?.replace(/"|'/g, ""),
config.tailwind.prefix
config.tailwind.prefix,
tailwindVersion
)}"`
)
}
@@ -156,7 +168,8 @@ export const transformTwPrefixes: Transformer = async ({
classNames.replaceWithText(
`"${applyPrefix(
classNames.getText()?.replace(/"|'/g, ""),
config.tailwind.prefix
config.tailwind.prefix,
tailwindVersion
)}"`
)
}
@@ -170,30 +183,49 @@ export const transformTwPrefixes: Transformer = async ({
return sourceFile
}
export function applyPrefix(input: string, prefix: string = "") {
const classNames = input.split(" ")
const prefixed: string[] = []
for (let className of classNames) {
const [variant, value, modifier] = splitClassName(className)
if (variant) {
modifier
? prefixed.push(`${variant}:${prefix}${value}/${modifier}`)
: prefixed.push(`${variant}:${prefix}${value}`)
} else {
modifier
? prefixed.push(`${prefix}${value}/${modifier}`)
: prefixed.push(`${prefix}${value}`)
}
export function applyPrefix(
input: string,
prefix: string = "",
tailwindVersion: TailwindVersion
) {
if (tailwindVersion === "v3") {
return input
.split(" ")
.map((className) => {
const [variant, value, modifier] = splitClassName(className)
if (variant) {
return modifier
? `${variant}:${prefix}${value}/${modifier}`
: `${variant}:${prefix}${value}`
} else {
return modifier
? `${prefix}${value}/${modifier}`
: `${prefix}${value}`
}
})
.join(" ")
}
return prefixed.join(" ")
return input
.split(" ")
.map((className) =>
className.indexOf(`${prefix}:`) === 0
? className
: `${prefix}:${className.trim()}`
)
.join(" ")
}
export function applyPrefixesCss(css: string, prefix: string) {
export function applyPrefixesCss(
css: string,
prefix: string,
tailwindVersion: TailwindVersion
) {
const lines = css.split("\n")
for (let line of lines) {
if (line.includes("@apply")) {
const originalTWCls = line.replace("@apply", "").trim()
const prefixedTwCls = applyPrefix(originalTWCls, prefix)
const prefixedTwCls = applyPrefix(originalTWCls, prefix, tailwindVersion)
css = css.replace(originalTWCls, prefixedTwCls)
}
}

View File

@@ -9,13 +9,16 @@ import prompts from "prompts"
export async function updateDependencies(
dependencies: RegistryItem["dependencies"],
devDependencies: RegistryItem["devDependencies"],
config: Config,
options: {
silent?: boolean
}
) {
dependencies = Array.from(new Set(dependencies))
if (!dependencies?.length) {
devDependencies = Array.from(new Set(devDependencies))
if (!dependencies?.length && !devDependencies?.length) {
return
}
@@ -59,23 +62,44 @@ export async function updateDependencies(
dependenciesSpinner?.start()
await execa(
packageManager,
[
packageManager === "npm" ? "install" : "add",
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
...dependencies,
],
{
cwd: config.resolvedPaths.cwd,
}
)
if (dependencies?.length) {
await execa(
packageManager,
[
packageManager === "npm" ? "install" : "add",
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
...(packageManager === "deno"
? dependencies.map((dep) => `npm:${dep}`)
: dependencies),
],
{
cwd: config.resolvedPaths.cwd,
}
)
}
if (devDependencies?.length) {
await execa(
packageManager,
[
packageManager === "npm" ? "install" : "add",
...(packageManager === "npm" && flag ? [`--${flag}`] : []),
"-D",
...(packageManager === "deno"
? devDependencies.map((dep) => `npm:${dep}`)
: devDependencies),
],
{
cwd: config.resolvedPaths.cwd,
}
)
}
dependenciesSpinner?.succeed()
}
function isUsingReact19(config: Config) {
const packageInfo = getPackageInfo(config.resolvedPaths.cwd)
const packageInfo = getPackageInfo(config.resolvedPaths.cwd, false)
if (!packageInfo?.dependencies?.react) {
return false

View File

@@ -1,4 +1,5 @@
import { existsSync, promises as fs } from "fs"
import { tmpdir } from "os"
import path, { basename } from "path"
import { getRegistryBaseColor } from "@/src/registry/api"
import { RegistryItem, registryItemFileSchema } from "@/src/registry/schema"
@@ -6,6 +7,7 @@ import { Config } from "@/src/utils/get-config"
import { ProjectInfo, getProjectInfo } from "@/src/utils/get-project-info"
import { highlighter } from "@/src/utils/highlighter"
import { logger } from "@/src/utils/logger"
import { resolveImport } from "@/src/utils/resolve-import"
import { spinner } from "@/src/utils/spinner"
import { transform } from "@/src/utils/transformers"
import { transformCssVars } from "@/src/utils/transformers/transform-css-vars"
@@ -14,6 +16,8 @@ import { transformImport } from "@/src/utils/transformers/transform-import"
import { transformRsc } from "@/src/utils/transformers/transform-rsc"
import { transformTwPrefixes } from "@/src/utils/transformers/transform-tw-prefix"
import prompts from "prompts"
import { Project, ScriptKind } from "ts-morph"
import { loadConfig } from "tsconfig-paths"
import { z } from "zod"
export async function updateFiles(
@@ -50,9 +54,9 @@ export async function updateFiles(
getRegistryBaseColor(config.tailwind.baseColor),
])
const filesCreated = []
const filesUpdated = []
const filesSkipped = []
let filesCreated: string[] = []
let filesUpdated: string[] = []
let filesSkipped: string[] = []
for (const file of files) {
if (!file.content) {
@@ -153,11 +157,25 @@ export async function updateFiles(
: filesCreated.push(path.relative(config.resolvedPaths.cwd, filePath))
}
const allFiles = [...filesCreated, ...filesUpdated, ...filesSkipped]
const updatedFiles = await resolveImports(allFiles, config)
// Let's update filesUpdated with the updated files.
filesUpdated.push(...updatedFiles)
// If a file is in filesCreated and filesUpdated, we should remove it from filesUpdated.
filesUpdated = filesUpdated.filter((file) => !filesCreated.includes(file))
const hasUpdatedFiles = filesCreated.length || filesUpdated.length
if (!hasUpdatedFiles && !filesSkipped.length) {
filesCreatedSpinner?.info("No files updated.")
}
// Remove duplicates.
filesCreated = Array.from(new Set(filesCreated))
filesUpdated = Array.from(new Set(filesUpdated))
filesSkipped = Array.from(new Set(filesSkipped))
if (filesCreated.length) {
filesCreatedSpinner?.succeed(
`Created ${filesCreated.length} ${
@@ -371,3 +389,227 @@ export function resolvePageTarget(
return ""
}
async function resolveImports(filePaths: string[], config: Config) {
const project = new Project({
compilerOptions: {},
})
const projectInfo = await getProjectInfo(config.resolvedPaths.cwd)
const tsConfig = await loadConfig(config.resolvedPaths.cwd)
const updatedFiles = []
if (!projectInfo || tsConfig.resultType === "failed") {
return []
}
for (const filepath of filePaths) {
const resolvedPath = path.resolve(config.resolvedPaths.cwd, filepath)
// Check if the file exists.
if (!existsSync(resolvedPath)) {
continue
}
const content = await fs.readFile(resolvedPath, "utf-8")
const dir = await fs.mkdtemp(path.join(tmpdir(), "shadcn-"))
const sourceFile = project.createSourceFile(
path.join(dir, basename(resolvedPath)),
content,
{
scriptKind: ScriptKind.TSX,
}
)
const importDeclarations = sourceFile.getImportDeclarations()
for (const importDeclaration of importDeclarations) {
const moduleSpecifier = importDeclaration.getModuleSpecifierValue()
// Filter out non-local imports.
if (
projectInfo?.aliasPrefix &&
!moduleSpecifier.startsWith(`${projectInfo.aliasPrefix}/`)
) {
continue
}
// Find the probable import file path.
// This is where we expect to find the file on disk.
const probableImportFilePath = await resolveImport(
moduleSpecifier,
tsConfig
)
if (!probableImportFilePath) {
continue
}
// Find the actual import file path.
// This is the path where the file has been installed.
const resolvedImportFilePath = resolveModuleByProbablePath(
probableImportFilePath,
filePaths,
config
)
if (!resolvedImportFilePath) {
continue
}
// Convert the resolved import file path to an aliased import.
const newImport = toAliasedImport(
resolvedImportFilePath,
config,
projectInfo
)
if (!newImport || newImport === moduleSpecifier) {
continue
}
importDeclaration.setModuleSpecifier(newImport)
// Write the updated content to the file.
await fs.writeFile(resolvedPath, sourceFile.getFullText(), "utf-8")
// Track the updated file.
updatedFiles.push(filepath)
}
}
return updatedFiles
}
/**
* Given an absolute "probable" import path (no ext),
* plus an array of absolute file paths you already know about,
* return 0N matches (best match first), and also check disk for any missing ones.
*/
export function resolveModuleByProbablePath(
probableImportFilePath: string,
files: string[],
config: Config,
extensions: string[] = [".tsx", ".ts", ".js", ".jsx", ".css"]
) {
const cwd = path.normalize(config.resolvedPaths.cwd)
// 1) Build a set of POSIX-normalized, project-relative files
const relativeFiles = files.map((f) => f.split(path.sep).join(path.posix.sep))
const fileSet = new Set(relativeFiles)
// 2) Strip any existing extension off the absolute base path
const extInPath = path.extname(probableImportFilePath)
const hasExt = extInPath !== ""
const absBase = hasExt
? probableImportFilePath.slice(0, -extInPath.length)
: probableImportFilePath
// 3) Compute the project-relative "base" directory for strong matching
const relBaseRaw = path.relative(cwd, absBase)
const relBase = relBaseRaw.split(path.sep).join(path.posix.sep)
// 4) Decide which extensions to try
const tryExts = hasExt ? [extInPath] : extensions
// 5) Collect candidates
const candidates = new Set<string>()
// 5a) Fastpath: [base + ext] and [base/index + ext]
for (const e of tryExts) {
const absCand = absBase + e
const relCand = path.posix.normalize(path.relative(cwd, absCand))
if (fileSet.has(relCand) || existsSync(absCand)) {
candidates.add(relCand)
}
const absIdx = path.join(absBase, `index${e}`)
const relIdx = path.posix.normalize(path.relative(cwd, absIdx))
if (fileSet.has(relIdx) || existsSync(absIdx)) {
candidates.add(relIdx)
}
}
// 5b) Fallback: scan known files by basename
const name = path.basename(absBase)
for (const f of relativeFiles) {
if (tryExts.some((e) => f.endsWith(`/${name}${e}`))) {
candidates.add(f)
}
}
// 6) If no matches, bail
if (candidates.size === 0) return null
// 7) Sort by (1) extension priority, then (2) "strong" base match
const sorted = Array.from(candidates).sort((a, b) => {
// a) extension order
const aExt = path.posix.extname(a)
const bExt = path.posix.extname(b)
const ord = tryExts.indexOf(aExt) - tryExts.indexOf(bExt)
if (ord !== 0) return ord
// b) strong match if path starts with relBase
const aStrong = relBase && a.startsWith(relBase) ? -1 : 1
const bStrong = relBase && b.startsWith(relBase) ? -1 : 1
return aStrong - bStrong
})
// 8) Return the first (best) candidate
return sorted[0]
}
export function toAliasedImport(
filePath: string,
config: Config,
projectInfo: ProjectInfo
): string | null {
const abs = path.normalize(path.join(config.resolvedPaths.cwd, filePath))
// 1⃣ Find the longest matching alias root in resolvedPaths
// e.g. key="ui", root="/…/components/ui" beats key="components"
const matches = Object.entries(config.resolvedPaths)
.filter(
([, root]) => root && abs.startsWith(path.normalize(root + path.sep))
)
.sort((a, b) => b[1].length - a[1].length)
if (matches.length === 0) {
return null
}
const [aliasKey, rootDir] = matches[0]
// 2⃣ Compute the path UNDER that root
let rel = path.relative(rootDir, abs)
// force POSIX-style separators
rel = rel.split(path.sep).join("/") // e.g. "button/index.tsx"
// 3⃣ Strip code-file extensions, keep others (css, json, etc.)
const ext = path.posix.extname(rel)
const codeExts = [".ts", ".tsx", ".js", ".jsx"]
const keepExt = codeExts.includes(ext) ? "" : ext
let noExt = rel.slice(0, rel.length - ext.length)
// 4⃣ Collapse "/index" to its directory
if (noExt.endsWith("/index")) {
noExt = noExt.slice(0, -"/index".length)
}
// 5⃣ Build the aliased path
// config.aliases[aliasKey] is e.g. "@/components/ui"
const aliasBase =
aliasKey === "cwd"
? projectInfo.aliasPrefix
: config.aliases[aliasKey as keyof typeof config.aliases]
if (!aliasBase) {
return null
}
// if noExt is empty (i.e. file was exactly at the root), we import the root
let suffix = noExt === "" ? "" : `/${noExt}`
// Rremove /src from suffix.
// Alias will handle this.
suffix = suffix.replace("/src", "")
// 6⃣ Prepend the prefix from projectInfo (e.g. "@") if needed
// but usually config.aliases already include it.
return `${aliasBase}${suffix}${keepExt}`
}

View File

@@ -0,0 +1 @@
{}

0
packages/shadcn/test/fixtures/project-deno/deno.lock generated vendored Normal file
View File

View File

@@ -0,0 +1,13 @@
{
"name": "test-cli-npm-project",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "npm-project",
"version": "1.0.0",
"license": "MIT"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"name": "test-cli-project-npm",
"version": "1.0.0",
"main": "index.js",
"author": "shadcn",
"license": "MIT",
"dependencies": {
"react": "19.0.0"
}
}

View File

@@ -3,7 +3,7 @@
exports[`transform tailwind prefix 1`] = `
"import * as React from "react"
export function Foo() {
return <div className="tw-bg-background hover:tw-bg-muted tw-text-primary-foreground sm:focus:tw-text-accent-foreground">foo</div>
return <div className="tw:bg-background hover:tw:bg-muted tw:text-primary-foreground sm:focus:tw:text-accent-foreground">foo</div>
}
"
`;
@@ -11,7 +11,7 @@ exports[`transform tailwind prefix 1`] = `
exports[`transform tailwind prefix 2`] = `
"import * as React from "react"
export function Foo() {
return <div className="tw-bg-white hover:tw-bg-stone-100 tw-text-stone-50 sm:focus:tw-text-stone-900 dark:tw-bg-stone-950 dark:hover:tw-bg-stone-800 dark:tw-text-stone-900 dark:sm:focus:tw-text-stone-50">foo</div>
return <div className="tw:bg-white hover:tw:bg-stone-100 tw:text-stone-50 sm:focus:tw:text-stone-900 dark:tw:bg-stone-950 dark:hover:tw:bg-stone-800 dark:tw:text-stone-900 dark:sm:focus:tw:text-stone-50">foo</div>
}
"
`;
@@ -19,7 +19,7 @@ export function Foo() {
exports[`transform tailwind prefix 3`] = `
"import * as React from "react"
export function Foo() {
return <div className={cn("tw-bg-white hover:tw-bg-stone-100 dark:tw-bg-stone-950 dark:hover:tw-bg-stone-800", true && "tw-text-stone-50 sm:focus:tw-text-stone-900 dark:tw-text-stone-900 dark:sm:focus:tw-text-stone-50")}>foo</div>
return <div className={cn("tw:bg-white hover:tw:bg-stone-100 dark:tw:bg-stone-950 dark:hover:tw:bg-stone-800", true && "tw:text-stone-50 sm:focus:tw:text-stone-900 dark:tw:text-stone-900 dark:sm:focus:tw:text-stone-50")}>foo</div>
}
"
`;
@@ -27,7 +27,7 @@ export function Foo() {
exports[`transform tailwind prefix 4`] = `
"import * as React from "react"
export function Foo() {
return <div className={cn("tw-bg-background hover:tw-bg-muted", true && "tw-text-primary-foreground sm:focus:tw-text-accent-foreground")}>foo</div>
return <div className={cn("tw:bg-background hover:tw:bg-muted", true && "tw:text-primary-foreground sm:focus:tw:text-accent-foreground")}>foo</div>
}
"
`;
@@ -105,10 +105,10 @@ exports[`transform tailwind prefix 5`] = `
@layer base {
* {
@apply tw-border-border;
@apply tw::border-border;
}
body {
@apply tw-bg-background tw-text-foreground;
@apply tw::bg-background tw::text-foreground;
}
}"
`;

View File

@@ -2,7 +2,7 @@ import { describe, expect, test } from "vitest"
import { applyPrefix } from "../../src/utils/transformers/transform-tw-prefix"
describe("apply tailwind prefix", () => {
describe("apply tailwind prefix v3", () => {
test.each([
{
input: "bg-slate-800 text-gray-500",
@@ -37,6 +37,45 @@ describe("apply tailwind prefix", () => {
"tw-absolute tw-right-4 tw-top-4 tw-bg-primary tw-rounded-sm tw-opacity-70 tw-ring-offset-background tw-transition-opacity hover:tw-opacity-100 focus:tw-outline-none focus:tw-ring-2 focus:tw-ring-ring focus:tw-ring-offset-2 disabled:tw-pointer-events-none data-[state=open]:tw-bg-secondary",
},
])(`applyTwPrefix($input) -> $output`, ({ input, output }) => {
expect(applyPrefix(input, "tw-")).toBe(output)
expect(applyPrefix(input, "tw-", "v3")).toBe(output)
})
})
describe("apply tailwind prefix v4", () => {
test.each([
{
input: "bg-slate-800 text-gray-500",
output: "tw:bg-slate-800 tw:text-gray-500",
},
{
input: "hover:dark:bg-background dark:text-foreground",
output: "tw:hover:dark:bg-background tw:dark:text-foreground",
},
{
input:
"rounded-lg border border-slate-200 bg-white text-slate-950 shadow-sm dark:border-slate-800 dark:bg-slate-950 dark:text-slate-50",
output:
"tw:rounded-lg tw:border tw:border-slate-200 tw:bg-white tw:text-slate-950 tw:shadow-sm tw:dark:border-slate-800 tw:dark:bg-slate-950 tw:dark:text-slate-50",
},
{
input:
"text-red-500 border-red-500/50 dark:border-red-500 [&>svg]:text-red-500 text-red-500 dark:text-red-900 dark:border-red-900/50 dark:dark:border-red-900 dark:[&>svg]:text-red-900 dark:text-red-900",
output:
"tw:text-red-500 tw:border-red-500/50 tw:dark:border-red-500 tw:[&>svg]:text-red-500 tw:text-red-500 tw:dark:text-red-900 tw:dark:border-red-900/50 tw:dark:dark:border-red-900 tw:dark:[&>svg]:text-red-900 tw:dark:text-red-900",
},
{
input:
"flex h-full w-full items-center justify-center rounded-full bg-muted",
output:
"tw:flex tw:h-full tw:w-full tw:items-center tw:justify-center tw:rounded-full tw:bg-muted",
},
{
input:
"absolute right-4 top-4 bg-primary rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary",
output:
"tw:absolute tw:right-4 tw:top-4 tw:bg-primary tw:rounded-sm tw:opacity-70 tw:ring-offset-background tw:transition-opacity tw:hover:opacity-100 tw:focus:outline-none tw:focus:ring-2 tw:focus:ring-ring tw:focus:ring-offset-2 tw:disabled:pointer-events-none tw:data-[state=open]:bg-secondary",
},
])(`applyTwPrefix($input) -> $output`, ({ input, output }) => {
expect(applyPrefix(input, "tw", "v4")).toBe(output)
})
})

View File

@@ -16,7 +16,7 @@ test("transform tailwind prefix", async () => {
config: {
tailwind: {
baseColor: "stone",
prefix: "tw-",
prefix: "tw:",
},
aliases: {
components: "@/components",
@@ -39,7 +39,7 @@ export function Foo() {
tailwind: {
baseColor: "stone",
cssVariables: false,
prefix: "tw-",
prefix: "tw:",
},
aliases: {
components: "@/components",
@@ -62,7 +62,7 @@ export function Foo() {
tailwind: {
baseColor: "stone",
cssVariables: false,
prefix: "tw-",
prefix: "tw:",
},
aliases: {
components: "@/components",
@@ -85,7 +85,7 @@ export function Foo() {
tailwind: {
baseColor: "stone",
cssVariables: false,
prefix: "tw-",
prefix: "tw:",
},
aliases: {
components: "@/components",
@@ -99,7 +99,8 @@ export function Foo() {
expect(
applyPrefixesCss(
"@tailwind base;\n@tailwind components;\n@tailwind utilities;\n \n@layer base {\n :root {\n --background: 0 0% 100%;\n --foreground: 224 71.4% 4.1%;\n \n --muted: 220 14.3% 95.9%;\n --muted-foreground: 220 8.9% 46.1%;\n \n --popover: 0 0% 100%;\n --popover-foreground: 224 71.4% 4.1%;\n \n --card: 0 0% 100%;\n --card-foreground: 224 71.4% 4.1%;\n \n --border: 220 13% 91%;\n --input: 220 13% 91%;\n \n --primary: 220.9 39.3% 11%;\n --primary-foreground: 210 20% 98%;\n \n --secondary: 220 14.3% 95.9%;\n --secondary-foreground: 220.9 39.3% 11%;\n \n --accent: 220 14.3% 95.9%;\n --accent-foreground: 220.9 39.3% 11%;\n \n --destructive: 0 84.2% 60.2%;\n --destructive-foreground: 210 20% 98%;\n \n --ring: 217.9 10.6% 64.9%;\n \n --radius: 0.5rem;\n }\n \n .dark {\n --background: 224 71.4% 4.1%;\n --foreground: 210 20% 98%;\n \n --muted: 215 27.9% 16.9%;\n --muted-foreground: 217.9 10.6% 64.9%;\n \n --popover: 224 71.4% 4.1%;\n --popover-foreground: 210 20% 98%;\n \n --card: 224 71.4% 4.1%;\n --card-foreground: 210 20% 98%;\n \n --border: 215 27.9% 16.9%;\n --input: 215 27.9% 16.9%;\n \n --primary: 210 20% 98%;\n --primary-foreground: 220.9 39.3% 11%;\n \n --secondary: 215 27.9% 16.9%;\n --secondary-foreground: 210 20% 98%;\n \n --accent: 215 27.9% 16.9%;\n --accent-foreground: 210 20% 98%;\n \n --destructive: 0 62.8% 30.6%;\n --destructive-foreground: 0 85.7% 97.3%;\n \n --ring: 215 27.9% 16.9%;\n }\n}\n \n@layer base {\n * {\n @apply border-border;\n }\n body {\n @apply bg-background text-foreground;\n }\n}",
"tw-"
"tw:",
"v4"
)
).toMatchSnapshot()
})

View File

@@ -0,0 +1,139 @@
import { vi, describe, afterEach, test, expect } from "vitest"
import { execa } from "execa"
import prompts from "prompts"
import { updateDependencies } from "../../../src/utils/updaters/update-dependencies"
import path from "path"
vi.mock("execa")
vi.mock("prompts")
describe("updateDependencies", () => {
afterEach(() => {
vi.restoreAllMocks()
})
test.each([
{
description: "npm without react 19 includes no additional flags",
options: { silent: true },
dependencies: ["first", "second", "third"],
devDependencies: ["fourth"],
config: {
resolvedPaths: {
cwd: path.resolve(__dirname, "../../fixtures/project-npm")
}
},
expectedPackageManager: "npm",
expectedArgs: ["install", "first", "second", "third"],
expectedDevArgs: ["install", "-D", "fourth"]
},
{
description: "npm with react 19 applies force prompt when silent",
options: { silent: true },
dependencies: ["first", "second", "third"],
devDependencies: ["fourth"],
config: {
resolvedPaths: {
cwd: path.resolve(__dirname, "../../fixtures/project-npm-react19")
}
},
expectedPackageManager: "npm",
expectedArgs: ["install", "--force", "first", "second", "third"],
expectedDevArgs: ["install", "--force", "-D", "fourth"]
},
{
description: "npm with react 19 prompts for flag when not silent",
flagPrompt: "legacy-peer-deps",
dependencies: ["first", "second", "third"],
devDependencies: ["fourth"],
config: {
resolvedPaths: {
cwd: path.resolve(__dirname, "../../fixtures/project-npm-react19")
}
},
expectedPackageManager: "npm",
expectedArgs: ["install", "--legacy-peer-deps", "first", "second", "third"],
expectedDevArgs: ["install", "--legacy-peer-deps", "-D", "fourth"]
},
{
description: "deno uses npm: package prefix",
dependencies: ["first", "second", "third"],
devDependencies: ["fourth"],
config: {
resolvedPaths: {
cwd: path.resolve(__dirname, "../../fixtures/project-deno")
}
},
expectedPackageManager: "deno",
expectedArgs: ["add", "npm:first", "npm:second", "npm:third"],
expectedDevArgs: ["add", "-D", "npm:fourth"]
},
{
description: "bun uses bun",
dependencies: ["first", "second", "third"],
devDependencies: ["fourth"],
config: {
resolvedPaths: {
cwd: path.resolve(__dirname, "../../fixtures/project-bun")
}
},
expectedPackageManager: "bun",
expectedArgs: ["add", "first", "second", "third"],
expectedDevArgs: ["add", "-D", "fourth"]
},
{
description: "pnpm uses pnpm",
dependencies: ["first", "second", "third"],
devDependencies: ["fourth"],
config: {
resolvedPaths: {
cwd: path.resolve(__dirname, "../../fixtures/project-pnpm")
}
},
expectedPackageManager: "pnpm",
expectedArgs: ["add", "first", "second", "third"],
expectedDevArgs: ["add", "-D", "fourth"]
},
{
description: "deduplicates input dependencies",
options: { silent: true },
dependencies: ["first", "first"],
devDependencies: ["second", "second"],
config: {
resolvedPaths: {
cwd: path.resolve(__dirname, "../../fixtures/project-npm")
}
},
expectedPackageManager: "npm",
expectedArgs: ["install", "first"],
expectedDevArgs: ["install", "-D", "second"]
}
])("$description", async ({ options, flagPrompt, config, dependencies, devDependencies, expectedPackageManager, expectedArgs, expectedDevArgs }) => {
vi.mocked(prompts).mockResolvedValue({ flag: flagPrompt })
await updateDependencies(
dependencies,
devDependencies,
config,
options ?? {}
)
if (flagPrompt) {
expect(prompts).toHaveBeenCalled()
}
expect(execa).toHaveBeenCalledWith(
expectedPackageManager,
expectedArgs,
{ cwd: config?.resolvedPaths.cwd }
)
expect(execa).toHaveBeenCalledWith(
expectedPackageManager,
expectedDevArgs,
{ cwd: config?.resolvedPaths.cwd }
)
})
})

View File

@@ -1,3 +1,4 @@
import { existsSync } from "fs"
import path from "path"
import { afterAll, afterEach, describe, expect, test, vi } from "vitest"
@@ -5,7 +6,9 @@ import { getConfig } from "../../../src/utils/get-config"
import {
findCommonRoot,
resolveFilePath,
resolveModuleByProbablePath,
resolveNestedFilePath,
toAliasedImport,
updateFiles,
} from "../../../src/utils/updaters/update-files"
@@ -809,3 +812,340 @@ return <div>Hello World</div>
`)
})
})
describe("resolveModuleByProbablePath", () => {
test("should resolve exact file match in provided files list", () => {
const files = [
"components/button.tsx",
"components/card.tsx",
"lib/utils.ts",
]
const config = {
resolvedPaths: {
cwd: "/foo/bar",
},
}
expect(
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
).toBe("components/button.tsx")
})
test("should resolve index file", () => {
const files = ["components/button/index.tsx", "components/card.tsx"]
const config = {
resolvedPaths: {
cwd: "/foo/bar",
},
}
expect(
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
).toBe("components/button/index.tsx")
})
test("should try different extensions", () => {
const files = ["components/button.jsx", "components/card.tsx"]
const config = {
resolvedPaths: {
cwd: "/foo/bar",
},
}
expect(
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
).toBe("components/button.jsx")
})
test("should fallback to basename matching", () => {
const files = ["components/ui/button.tsx", "components/card.tsx"]
const config = {
resolvedPaths: {
cwd: "/foo/bar",
},
}
expect(
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
).toBe("components/ui/button.tsx")
})
test("should return null when file not found", () => {
const files = ["components/card.tsx", "lib/utils.ts"]
const config = {
resolvedPaths: {
cwd: "/foo/bar",
},
}
expect(
resolveModuleByProbablePath("/foo/bar/components/button", files, config)
).toBeNull()
})
test("should sort by extension priority", () => {
const files = [
"components/button.jsx",
"components/button.tsx",
"components/button.js",
]
const config = {
resolvedPaths: {
cwd: "/foo/bar",
},
}
expect(
resolveModuleByProbablePath("/foo/bar/components/button", files, config, [
".tsx",
".jsx",
".js",
])
).toBe("components/button.tsx")
})
test("should preserve extension if specified in path", () => {
const files = ["components/button.tsx", "components/button.css"]
const config = {
resolvedPaths: {
cwd: "/foo/bar",
},
}
expect(
resolveModuleByProbablePath(
"/foo/bar/components/button.css",
files,
config
)
).toBe("components/button.css")
})
})
describe("toAliasedImport", () => {
test("should convert components path to aliased import", () => {
const filePath = "components/button.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
lib: "/foo/bar/lib",
},
aliases: {
components: "@/components",
ui: "@/components/ui",
lib: "@/lib",
},
}
const projectInfo = {
aliasPrefix: "@",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"@/components/button"
)
})
test("should convert ui path to aliased import", () => {
const filePath = "components/ui/button.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
lib: "/foo/bar/lib",
},
aliases: {
components: "@/components",
ui: "@/components/ui",
lib: "@/lib",
},
}
const projectInfo = {
aliasPrefix: "@",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"@/components/ui/button"
)
})
test("should collapse index files", () => {
const filePath = "components/ui/button/index.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
lib: "/foo/bar/lib",
},
aliases: {
components: "@/components",
ui: "@/components/ui",
lib: "@/lib",
},
}
const projectInfo = {
aliasPrefix: "@",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"@/components/ui/button"
)
})
test("should return null when no matching alias found", () => {
const filePath = "src/pages/index.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
lib: "/foo/bar/lib",
},
aliases: {
components: "@/components",
ui: "@/components/ui",
lib: "@/lib",
},
}
const projectInfo = {
aliasPrefix: "@",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe("@/pages")
})
test("should handle nested directories", () => {
const filePath = "components/forms/inputs/text-input.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
lib: "/foo/bar/lib",
},
aliases: {
components: "@/components",
ui: "@/components/ui",
lib: "@/lib",
},
}
const projectInfo = {
aliasPrefix: "@",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"@/components/forms/inputs/text-input"
)
})
test("should keep non-code file extensions", () => {
const filePath = "components/styles/theme.css"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
lib: "/foo/bar/lib",
},
aliases: {
components: "@/components",
ui: "@/components/ui",
lib: "@/lib",
},
}
const projectInfo = {
aliasPrefix: "@",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"@/components/styles/theme.css"
)
})
test("should prefer longer matching paths", () => {
const filePath = "components/ui/button.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
},
aliases: {
components: "@/components",
ui: "@/ui",
},
}
const projectInfo = {
aliasPrefix: "@",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe("@/ui/button")
})
test("should support tilde (~) alias prefix", () => {
const filePath = "components/button.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
},
aliases: {
components: "~components",
},
}
const projectInfo = {
aliasPrefix: "~",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"~components/button"
)
})
test("should support @shadcn alias prefix", () => {
const filePath = "components/ui/button.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
},
aliases: {
components: "@shadcn/components",
ui: "@shadcn/ui",
},
}
const projectInfo = {
aliasPrefix: "@shadcn",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe(
"@shadcn/ui/button"
)
})
test("should support ~cn alias prefix", () => {
const filePath = "lib/utils/index.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
lib: "/foo/bar/lib",
},
aliases: {
lib: "~cn/lib",
},
}
const projectInfo = {
aliasPrefix: "~cn",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe("~cn/lib/utils")
})
test("should use project alias prefix when aliasKey is cwd", () => {
const filePath = "src/pages/home.tsx"
const config = {
resolvedPaths: {
cwd: "/foo/bar",
components: "/foo/bar/components",
ui: "/foo/bar/components/ui",
lib: "/foo/bar/lib",
},
aliases: {
components: "@/components",
ui: "@/components/ui",
lib: "@/lib",
},
}
const projectInfo = {
aliasPrefix: "@",
}
expect(toAliasedImport(filePath, config, projectInfo)).toBe("@/pages/home")
})
})

4
pnpm-lock.yaml generated
View File

@@ -283,7 +283,7 @@ importers:
specifier: ^6.0.1
version: 6.0.1
shadcn:
specifier: 2.4.1
specifier: 2.5.0
version: link:../../packages/shadcn
sonner:
specifier: ^2.0.0
@@ -542,7 +542,7 @@ importers:
specifier: 2.12.7
version: 2.12.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
shadcn:
specifier: 2.4.1
specifier: 2.5.0
version: link:../../packages/shadcn
sharp:
specifier: ^0.32.6