Compare commits

...

30 Commits

Author SHA1 Message Date
github-actions[bot]
1289192d4f chore(release): version packages (#8231)
* chore(release): version packages

* chore: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-09-17 08:17:57 +04:00
shadcn
75dde2e646 fix(shadcn): deps in cts projects (#8229)
* fix(shadcn): deps in cts projects

* fix: deps

* chore: add changelog
2025-09-16 17:54:44 +04:00
github-actions[bot]
b9f3ce1988 chore(release): version packages (#8217)
* chore(release): version packages

* chore(release): version packages

* ci: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 16:27:20 +04:00
Elliot Sutton
cdf58be7e1 feat(shadcn): fix transformCssVars function (#8186)
* feat(shadcn): fix transformCssVars function

* test(shadcn): update snapshots

* chore: add changeset

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 16:08:56 +04:00
Fuma Nama
fae1a81add fix(shadcn): fix async imports not being transformed (#8036)
* fix(shadcn): fix async imports not being transformed when installing components

* fix(shadcn): improve performance

* test(shadcn): add tests for transform import

* test: update timeout

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 14:55:18 +04:00
shadcn
fc6d909ba2 add getRegistriesIndex (#8216)
* feat: add getRegistriesIndex

* chore: changeset

* fix: formatting
2025-09-15 14:55:05 +04:00
shadcn
590b9be610 fix: toc 2025-09-15 10:52:27 +04:00
Dillion Verma
41eb9d5c46 fix: update magicui registry name (#8214)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 10:48:43 +04:00
shadcn
b7c28199be docs: add import and plugin examples (#8215) 2025-09-15 10:46:58 +04:00
Maximilian Kaske
7869defd42 feat(charts): support legend and tooltip type none (#8082)
* feat(charts): support legend and tooltip type none

* fix: format

* chore: run registry:build

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-09-15 09:14:47 +04:00
Itzik Sokolov
6daa5215cc Add 97cn to registrries.json (#8207) 2025-09-15 08:34:42 +04:00
WebDevSimplified
722fb81b95 Add WDS registry URL to registries.json (#8193)
I am attempting to add the [Web Dev Simplified Shadcn Registry](https://wds-shadcn-registry.netlify.app) as an indexed registry.
2025-09-11 15:56:28 +04:00
Elliot Hesp
543be31722 docs: fix bad link to registry index (#8184) 2025-09-09 20:19:58 +04:00
Talha Mujahid
09b90cd5c2 Add @shadcn-editor registry URL to registries.json (#8177)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-09 17:09:09 +04:00
Nitish
c95959a9b3 added rigidui to trusted registries (#8180)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-09 17:06:31 +04:00
Sean
08820ce5ee feat: add @reui in trusted registries (#8181) 2025-09-09 17:04:45 +04:00
Arif Hossain
cb96e58992 feat: add @retroui in trusted registries (#8167)
* feat: add @retroui in trusted registries

* merge conflict resolve
2025-09-09 17:02:28 +04:00
Ali Hussein
fce5926265 Add formcn.dev to trusted registries (#8163)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-08 16:04:47 +04:00
Rohan Gupta
f7c0f81258 feat: added limeplay registry (#8174)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-08 15:40:36 +04:00
Gxuri
960b22b301 feat: add @skiper-ui to trusted registries (#8170)
* feat: add @skiper-ui to trusted registries

* Fix URL for @skiper-ui registry

---------

Co-authored-by: shadcn <m@shadcn.com>
2025-09-08 15:37:32 +04:00
Elliot Sutton
6f057c9cc3 feat(v4): add @animate-ui to trusted registries (#8162) 2025-09-08 15:31:51 +04:00
shadcn
615a32d97a Merge branch 'main' of github.com:shadcn-ui/ui 2025-09-04 20:42:21 +04:00
shadcn
bfe6e1946c docs: update changelog 2025-09-04 20:41:50 +04:00
Railly Hugo
baaa82e4e7 feat: add @elements to trusted registries (#8155)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 20:29:52 +04:00
Alex Carpenter
caeed7bd65 feat: Add @alexcarpenter registry to known registries (#8154) 2025-09-04 20:26:49 +04:00
Alex Carpenter
61254f0c3f feat: add clerk to known registries (#8153)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 20:24:38 +04:00
Edu Calvo
3dcd797f2c feat: add @smoothui to trusted registries (#8152)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 20:24:24 +04:00
Théo Ribbi
b76f5cdbf7 feat: add @nativeui to trusted registries (#8146)
Co-authored-by: shadcn <m@shadcn.com>
2025-09-04 20:20:15 +04:00
Ephraim Duncan
fcb1e2ca50 Add blocks registry URL to registries.json (#8145) 2025-09-04 20:18:48 +04:00
Sahil Tiwaskar
df94537e0f add billingsdk registry (#8148) 2025-09-04 16:01:35 +04:00
30 changed files with 1121 additions and 537 deletions

View File

@@ -30,9 +30,13 @@ const TOP_LEVEL_SECTIONS = [
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Changelog",
href: "/docs/changelog",
},
]
const EXCLUDED_SECTIONS = ["installation", "dark-mode"]
const EXCLUDED_PAGES = ["/docs"]
const EXCLUDED_PAGES = ["/docs", "/docs/changelog"]
export function DocsSidebar({
tree,

View File

@@ -28,6 +28,10 @@ const TOP_LEVEL_SECTIONS = [
name: "MCP Server",
href: "/docs/mcp",
},
{
name: "Changelog",
href: "/docs/changelog",
},
]
export function MobileNav({

View File

@@ -4,6 +4,24 @@ description: Latest updates and announcements.
toc: false
---
## September 2025 - Registry Index
We've created an index of open source registries that you can install items from.
You can search, view and add items from the registry index without configuring the `.components.json` file.
They'll be automatically added to your `components.json` file for you.
```bash
npx shadcn add @ai-elements/prompt-input
```
The full list of registries is available at [https://ui.shadcn.com/r/registries.json](https://ui.shadcn.com/r/registries.json).
To add a registry to the index, submit a PR to the `shadcn/ui` repository. See the [registry index documentation](/docs/registry/registry-index) for more details.
---
## August 2025 - shadcn CLI 3.0 and MCP Server
We just shipped shadcn CLI 3.0 with support for namespaced registries, advanced authentication, new commands and a completely rewritten registry engine.

View File

@@ -328,16 +328,153 @@ Add custom theme variables to the `theme` object.
}
```
## Add CSS imports
Use `@import` to add CSS imports to your registry item. The imports will be placed at the top of the CSS file.
### Basic import
```json title="example-import.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "custom-import",
"type": "registry:component",
"css": {
"@import \"tailwindcss\"": {},
"@import \"./styles/base.css\"": {}
}
}
```
### Import with url() syntax
```json title="example-url-import.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "font-import",
"type": "registry:item",
"css": {
"@import url(\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap\")": {},
"@import url('./local-styles.css')": {}
}
}
```
### Import with media queries
```json title="example-media-import.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "responsive-import",
"type": "registry:item",
"css": {
"@import \"print-styles.css\" print": {},
"@import url(\"mobile.css\") screen and (max-width: 768px)": {}
}
}
```
## Add custom plugins
Use `@plugin` to add Tailwind plugins to your registry item. Plugins will be automatically placed after imports and before other content.
**Important:** When using plugins from npm packages, you must also add them to the `dependencies` array.
### Basic plugin usage
```json title="example-plugin.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "custom-plugin",
"type": "registry:item",
"css": {
"@plugin \"@tailwindcss/typography\"": {},
"@plugin \"foo\"": {}
}
}
```
### Plugin with npm dependency
When using plugins from npm packages like `@tailwindcss/typography`, include them in the dependencies.
```json title="example-typography.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "typography-component",
"type": "registry:item",
"dependencies": ["@tailwindcss/typography"],
"css": {
"@plugin \"@tailwindcss/typography\"": {},
"@layer components": {
".prose": {
"max-width": "65ch"
}
}
}
}
```
### Scoped and file-based plugins
```json title="example-scoped-plugin.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "scoped-plugins",
"type": "registry:component",
"css": {
"@plugin @tailwindcss/typography": {},
"@plugin foo": {}
"@plugin \"@headlessui/tailwindcss\"": {},
"@plugin \"tailwindcss/plugin\"": {},
"@plugin \"./custom-plugin.js\"": {}
}
}
```
### Multiple plugins with automatic ordering
When you add multiple plugins, they are automatically grouped together and deduplicated.
```json title="example-multiple-plugins.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "multiple-plugins",
"type": "registry:item",
"dependencies": [
"@tailwindcss/typography",
"@tailwindcss/forms",
"tw-animate-css"
],
"css": {
"@plugin \"@tailwindcss/typography\"": {},
"@plugin \"@tailwindcss/forms\"": {},
"@plugin \"tw-animate-css\"": {}
}
}
```
## Combined imports and plugins
When using both `@import` and `@plugin` directives, imports are placed first, followed by plugins, then other CSS content.
```json title="example-combined.json" showLineNumbers
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "combined-example",
"type": "registry:item",
"dependencies": ["@tailwindcss/typography", "tw-animate-css"],
"css": {
"@import \"tailwindcss\"": {},
"@import url(\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap\")": {},
"@plugin \"@tailwindcss/typography\"": {},
"@plugin \"tw-animate-css\"": {},
"@layer base": {
"body": {
"font-family": "Inter, sans-serif"
}
},
"@utility content-auto": {
"content-visibility": "auto"
}
}
}
```

View File

@@ -41,7 +41,7 @@ Registry namespaces are prefixed with `@` and provide a way to organize and refe
## Decentralized Namespace System
We intentionally designed the namespace system to be decentralized. There is [central registrar](/docs/registry/registrar) for open source namespaces but you are free to create and use any namespace you want.
We intentionally designed the namespace system to be decentralized. There is a [central open source registry index](/docs/registry/registry-index) for open source namespaces but you are free to create and use any namespace you want.
This decentralized approach gives you complete flexibility to organize your resources however makes sense for your organization.

View File

@@ -87,7 +87,7 @@
"recharts": "2.15.1",
"rehype-pretty-code": "^0.14.1",
"rimraf": "^6.0.1",
"shadcn": "3.2.1",
"shadcn": "3.3.1",
"shiki": "^1.10.1",
"sonner": "^2.0.0",
"tailwind-merge": "^3.0.1",

View File

@@ -1,19 +1,36 @@
{
"@aceternity": "https://ui.aceternity.com/registry/{name}.json",
"@ai-elements": "https://registry.ai-sdk.dev/{name}.json",
"@alexcarpenter": "https://ui.alexcarpenter.me/r/{name}.json",
"@alpine": "https://alpine-registry.vercel.app/r/{name}.json",
"@animate-ui": "https://animate-ui.com/r/{name}.json",
"@blocks": "https://blocks.so/r/{name}.json",
"@clerk": "https://clerk.com/r/{name}.json",
"@cult-ui": "https://cult-ui.com/r/{name}.json",
"@kibo-ui": "https://www.kibo-ui.com/r/{name}.json",
"@kokonutui": "https://kokonutui.com/r/{name}.json",
"@magic-ui": "https://magicui.design/r/{name}.json",
"@magicui": "https://magicui.design/r/{name}.json",
"@motion-primitives": "https://motion-primitives.com/c/{name}.json",
"@originui": "https://originui.com/r/{name}.json",
"@prompt-kit": "https://prompt-kit.com/c/{name}.json",
"@tailark": "https://tailark.com/r/{name}.json",
"@react-bits": "https://reactbits.dev/r/{name}.json",
"@reui": "https://reui.io/r/{name}.json",
"@heseui": "https://www.heseui.com/r/{name}.json",
"@paceui-ui": "https://ui.paceui.com/r/{name}.json",
"@basecn": "https://basecn.dev/r/{name}.json",
"@ncdai": "https://chanhdai.com/r/{name}.json",
"@8bitcn": "https://8bitcn.com/r/{name}.json"
"@8bitcn": "https://8bitcn.com/r/{name}.json",
"@billingsdk": "https://billingsdk.com/r/{name}.json",
"@elements": "https://tryelements.dev/r/{name}.json",
"@nativeui": "https://nativeui.io/registry/{name}.json",
"@smoothui": "https://smoothui.dev/r/{name}.json",
"@formcn": "https://formcn.dev/r/{name}.json",
"@limeplay": "https://limeplay.winoffrg.dev/r/{name}.json",
"@skiper-ui": "https://skiper-ui.com/registry/{name}.json",
"@shadcn-editor": "https://shadcn-editor.vercel.app/r/{name}.json",
"@rigidui": "https://rigidui.com/r/{name}.json",
"@retroui": "https://retroui.dev/r/{name}.json",
"@wds": "https://wds-shadcn-registry.netlify.app/r/{name}.json",
"@97cn": "https://97cn.itzik.co/r/{name}.json"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -179,70 +179,72 @@ function ChartTooltipContent({
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
return (
<div
key={item.dataKey}
className={cn(
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
/>
)
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
{item.value && (
<span className="text-foreground font-mono font-medium tabular-nums">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</>
)}
</div>
)
})}
</div>
</div>
)
@@ -275,31 +277,33 @@ function ChartLegendContent({
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
return (
<div
key={item.value}
className={cn(
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}

View File

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

View File

@@ -185,70 +185,72 @@ const ChartTooltipContent = React.forwardRef<
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
/>
)
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</>
)}
</div>
)
})}
</div>
</div>
)
@@ -285,31 +287,33 @@ const ChartLegendContent = React.forwardRef<
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}

View File

@@ -185,70 +185,72 @@ const ChartTooltipContent = React.forwardRef<
>
{!nestLabel ? tooltipLabel : null}
<div className="grid gap-1.5">
{payload.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
{payload
.filter((item) => item.type !== "none")
.map((item, index) => {
const key = `${nameKey || item.name || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
const indicatorColor = color || item.payload.fill || item.color
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
return (
<div
key={item.dataKey}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center"
)}
>
{formatter && item?.value !== undefined && item.name ? (
formatter(item.value, item.name, item, index, item.payload)
) : (
<>
{itemConfig?.icon ? (
<itemConfig.icon />
) : (
!hideIndicator && (
<div
className={cn(
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
{
"h-2.5 w-2.5": indicator === "dot",
"w-1": indicator === "line",
"w-0 border-[1.5px] border-dashed bg-transparent":
indicator === "dashed",
"my-0.5": nestLabel && indicator === "dashed",
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
)}
style={
{
"--color-bg": indicatorColor,
"--color-border": indicatorColor,
} as React.CSSProperties
}
/>
)
)}
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
/>
)
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
<div
className={cn(
"flex flex-1 justify-between leading-none",
nestLabel ? "items-end" : "items-center"
)}
>
<div className="grid gap-1.5">
{nestLabel ? tooltipLabel : null}
<span className="text-muted-foreground">
{itemConfig?.label || item.name}
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
{item.value.toLocaleString()}
</span>
)}
</div>
</>
)}
</div>
)
})}
</>
)}
</div>
)
})}
</div>
</div>
)
@@ -285,31 +287,33 @@ const ChartLegendContent = React.forwardRef<
className
)}
>
{payload.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
{payload
.filter((item) => item.type !== "none")
.map((item) => {
const key = `${nameKey || item.dataKey || "value"}`
const itemConfig = getPayloadConfigFromPayload(config, item, key)
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
return (
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
)}
>
{itemConfig?.icon && !hideIcon ? (
<itemConfig.icon />
) : (
<div
className="h-2 w-2 shrink-0 rounded-[2px]"
style={{
backgroundColor: item.color,
}}
/>
)}
{itemConfig?.label}
</div>
)
})}
</div>
)
}

View File

@@ -1,5 +1,23 @@
# @shadcn/ui
## 3.3.1
### Patch Changes
- [`75dde2e64679081dad63ecd20f3fd9e3b68fa0ce`](https://github.com/shadcn-ui/ui/commit/75dde2e64679081dad63ecd20f3fd9e3b68fa0ce) Thanks [@shadcn](https://github.com/shadcn)! - fix deps in cts projects
## 3.3.0
### Minor Changes
- [#8216](https://github.com/shadcn-ui/ui/pull/8216) [`fc6d909ba23ac1ba09cf32087f0524aca398b5aa`](https://github.com/shadcn-ui/ui/commit/fc6d909ba23ac1ba09cf32087f0524aca398b5aa) Thanks [@shadcn](https://github.com/shadcn)! - add getRegistriesIndex
### Patch Changes
- [#8186](https://github.com/shadcn-ui/ui/pull/8186) [`cdf58be7e1ed25bf1dd19a1c60612c5e89b82a60`](https://github.com/shadcn-ui/ui/commit/cdf58be7e1ed25bf1dd19a1c60612c5e89b82a60) Thanks [@imskyleen](https://github.com/imskyleen)! - fix transformCssVars function with prefix
- [#8036](https://github.com/shadcn-ui/ui/pull/8036) [`fae1a81addb22429c103d5d08813e1c80779d5fb`](https://github.com/shadcn-ui/ui/commit/fae1a81addb22429c103d5d08813e1c80779d5fb) Thanks [@fuma-nama](https://github.com/fuma-nama)! - fix async imports not being transformed when installing components
## 3.2.1
### Patch Changes

View File

@@ -1,6 +1,6 @@
{
"name": "shadcn",
"version": "3.2.1",
"version": "3.3.1",
"description": "Add components to your apps.",
"publishConfig": {
"access": "public"
@@ -68,8 +68,10 @@
"@babel/core": "^7.28.0",
"@babel/parser": "^7.28.0",
"@babel/plugin-transform-typescript": "^7.28.0",
"@babel/preset-typescript": "^7.27.1",
"@dotenvx/dotenvx": "^1.48.4",
"@modelcontextprotocol/sdk": "^1.17.2",
"browserslist": "^4.26.2",
"commander": "^14.0.0",
"cosmiconfig": "^9.0.0",
"dedent": "^1.6.0",

View File

@@ -27,7 +27,13 @@ import {
} from "vitest"
import { z } from "zod"
import { getRegistriesConfig, getRegistry, getRegistryItems } from "./api"
import {
getRegistriesConfig,
getRegistriesIndex,
getRegistry,
getRegistryItems,
} from "./api"
import { RegistriesIndexParseError } from "./errors"
vi.mock("@/src/utils/handle-error", () => ({
handleError: vi.fn(),
@@ -96,6 +102,13 @@ const server = setupServer(
},
],
})
}),
http.get(`${REGISTRY_URL}/registries.json`, () => {
return HttpResponse.json({
"@shadcn": "https://ui.shadcn.com/r/styles/{style}/{name}.json",
"@example": "https://example.com/registry/styles/{style}/{name}.json",
"@test": "https://test.com/registry/{name}.json",
})
})
)
@@ -1650,4 +1663,75 @@ describe("getRegistriesConfig", () => {
}
})
})
describe("getRegistriesIndex", () => {
it("should fetch and parse the registries index successfully", async () => {
const result = await getRegistriesIndex()
expect(result).toEqual({
"@shadcn": "https://ui.shadcn.com/r/styles/{style}/{name}.json",
"@example": "https://example.com/registry/styles/{style}/{name}.json",
"@test": "https://test.com/registry/{name}.json",
})
})
it("should respect cache options", async () => {
// Test with cache disabled
const result1 = await getRegistriesIndex({ useCache: false })
expect(result1).toBeDefined()
// Test with cache enabled
const result2 = await getRegistriesIndex({ useCache: true })
expect(result2).toBeDefined()
// Results should be the same
expect(result1).toEqual(result2)
})
it("should use default cache behavior when no options provided", async () => {
const result = await getRegistriesIndex()
expect(result).toBeDefined()
expect(typeof result).toBe("object")
})
it("should handle network errors properly", async () => {
server.use(
http.get(`${REGISTRY_URL}/registries.json`, () => {
return new HttpResponse(null, { status: 500 })
})
)
await expect(getRegistriesIndex({ useCache: false })).rejects.toThrow()
try {
await getRegistriesIndex({ useCache: false })
} catch (error) {
expect(error).not.toBeInstanceOf(RegistriesIndexParseError)
}
})
it("should handle invalid JSON response", async () => {
server.use(
http.get(`${REGISTRY_URL}/registries.json`, () => {
return HttpResponse.json({
"invalid-namespace": "some-url",
})
})
)
await expect(getRegistriesIndex({ useCache: false })).rejects.toThrow(
RegistriesIndexParseError
)
})
it("should handle network timeout", async () => {
server.use(
http.get(`${REGISTRY_URL}/registries.json`, () => {
return HttpResponse.error()
})
)
await expect(getRegistriesIndex({ useCache: false })).rejects.toThrow()
})
})
})

View File

@@ -12,6 +12,7 @@ import {
} from "@/src/registry/context"
import {
ConfigParseError,
RegistriesIndexParseError,
RegistryInvalidNamespaceError,
RegistryNotFoundError,
RegistryParseError,
@@ -277,15 +278,24 @@ export async function getItemTargetPath(
)
}
export async function fetchRegistries() {
export async function getRegistriesIndex(options?: { useCache?: boolean }) {
options = {
useCache: true,
...options,
}
const url = `${REGISTRY_URL}/registries.json`
const [data] = await fetchRegistry([url], {
useCache: options.useCache,
})
try {
// TODO: Do we want this inside /r?
const url = `${REGISTRY_URL}/registries.json`
const [data] = await fetchRegistry([url], {
useCache: process.env.NODE_ENV !== "development",
})
return registriesIndexSchema.parse(data)
} catch {
return null
} catch (error) {
if (error instanceof z.ZodError) {
throw new RegistriesIndexParseError(error)
}
throw error
}
}

View File

@@ -285,3 +285,41 @@ export class ConfigParseError extends RegistryError {
this.name = "ConfigParseError"
}
}
export class RegistriesIndexParseError extends RegistryError {
public readonly parseError: unknown
constructor(parseError: unknown) {
let message = "Failed to parse registries index"
if (parseError instanceof z.ZodError) {
const invalidNamespaces = parseError.errors
.filter((e) => e.path.length > 0)
.map((e) => `"${e.path[0]}"`)
.filter((v, i, arr) => arr.indexOf(v) === i) // remove duplicates
if (invalidNamespaces.length > 0) {
message = `Failed to parse registries index. Invalid registry namespace(s): ${invalidNamespaces.join(
", "
)}\n${parseError.errors
.map((e) => ` - ${e.path.join(".")}: ${e.message}`)
.join("\n")}`
} else {
message = `Failed to parse registries index:\n${parseError.errors
.map((e) => ` - ${e.path.join(".")}: ${e.message}`)
.join("\n")}`
}
}
super(message, {
code: RegistryErrorCode.PARSE_ERROR,
cause: parseError,
context: { parseError },
suggestion:
"The registries index may be corrupted or have invalid registry namespace format. Registry names must start with @ (e.g., @shadcn, @example).",
})
this.parseError = parseError
this.name = "RegistriesIndexParseError"
}
}

View File

@@ -1,4 +1,9 @@
export { getRegistryItems, resolveRegistryItems, getRegistry } from "./api"
export {
getRegistryItems,
resolveRegistryItems,
getRegistry,
getRegistriesIndex,
} from "./api"
export { searchRegistries } from "./search"
@@ -11,6 +16,7 @@ export {
RegistryNotConfiguredError,
RegistryLocalFileError,
RegistryParseError,
RegistriesIndexParseError,
RegistryMissingEnvironmentVariablesError,
RegistryInvalidNamespaceError,
} from "./errors"

View File

@@ -117,239 +117,228 @@ export async function resolveRegistryTree(
config: Config,
options: { useCache?: boolean } = {}
) {
try {
options = {
useCache: true,
...options,
options = {
useCache: true,
...options,
}
let payload: z.infer<typeof registryItemWithSourceSchema>[] = []
let allDependencyItems: z.infer<typeof registryItemWithSourceSchema>[] = []
let allDependencyRegistryNames: string[] = []
const uniqueNames = Array.from(new Set(names))
const results = await fetchRegistryItems(uniqueNames, config, options)
const resultMap = new Map<string, z.infer<typeof registryItemSchema>>()
for (let i = 0; i < results.length; i++) {
if (results[i]) {
resultMap.set(uniqueNames[i], results[i])
}
}
let payload: z.infer<typeof registryItemWithSourceSchema>[] = []
let allDependencyItems: z.infer<typeof registryItemWithSourceSchema>[] = []
let allDependencyRegistryNames: string[] = []
const uniqueNames = Array.from(new Set(names))
const results = await fetchRegistryItems(uniqueNames, config, options)
const resultMap = new Map<string, z.infer<typeof registryItemSchema>>()
for (let i = 0; i < results.length; i++) {
if (results[i]) {
resultMap.set(uniqueNames[i], results[i])
}
for (const [sourceName, item] of Array.from(resultMap.entries())) {
// Add source tracking
const itemWithSource: z.infer<typeof registryItemWithSourceSchema> = {
...item,
_source: sourceName,
}
payload.push(itemWithSource)
for (const [sourceName, item] of Array.from(resultMap.entries())) {
// Add source tracking
const itemWithSource: z.infer<typeof registryItemWithSourceSchema> = {
...item,
_source: sourceName,
}
payload.push(itemWithSource)
if (item.registryDependencies) {
// Resolve namespace syntax and set headers for dependencies
let resolvedDependencies = item.registryDependencies
if (item.registryDependencies) {
// Resolve namespace syntax and set headers for dependencies
let resolvedDependencies = item.registryDependencies
// Check for namespaced dependencies when no registries are configured
if (!config?.registries) {
const namespacedDeps = item.registryDependencies.filter(
(dep: string) => dep.startsWith("@")
)
if (namespacedDeps.length > 0) {
const { registry } = parseRegistryAndItemFromString(
namespacedDeps[0]
)
throw new RegistryNotConfiguredError(registry)
}
} else {
resolvedDependencies = resolveRegistryItemsFromRegistries(
item.registryDependencies,
config
)
}
const { items, registryNames } = await resolveDependenciesRecursively(
resolvedDependencies,
config,
options,
new Set(uniqueNames)
// Check for namespaced dependencies when no registries are configured
if (!config?.registries) {
const namespacedDeps = item.registryDependencies.filter((dep: string) =>
dep.startsWith("@")
)
allDependencyItems.push(...items)
allDependencyRegistryNames.push(...registryNames)
}
}
payload.push(...allDependencyItems)
// Handle any remaining registry names that need index resolution
if (allDependencyRegistryNames.length > 0) {
// Remove duplicates from registry names
const uniqueRegistryNames = Array.from(
new Set(allDependencyRegistryNames)
)
// Separate namespaced and non-namespaced items
const nonNamespacedItems = uniqueRegistryNames.filter(
(name) => !name.startsWith("@")
)
const namespacedDepItems = uniqueRegistryNames.filter((name) =>
name.startsWith("@")
)
// Handle namespaced dependency items
if (namespacedDepItems.length > 0) {
// This will now throw specific errors on failure
const depResults = await fetchRegistryItems(
namespacedDepItems,
config,
options
if (namespacedDeps.length > 0) {
const { registry } = parseRegistryAndItemFromString(namespacedDeps[0])
throw new RegistryNotConfiguredError(registry)
}
} else {
resolvedDependencies = resolveRegistryItemsFromRegistries(
item.registryDependencies,
config
)
for (let i = 0; i < depResults.length; i++) {
const item = depResults[i]
const itemWithSource: z.infer<typeof registryItemWithSourceSchema> = {
...item,
_source: namespacedDepItems[i],
}
payload.push(itemWithSource)
}
}
// For non-namespaced items, we need the index and style resolution
if (nonNamespacedItems.length > 0) {
const index = await getShadcnRegistryIndex()
if (!index && payload.length === 0) {
return null
}
if (index) {
// If we're resolving the index, we want it to go first
if (nonNamespacedItems.includes("index")) {
nonNamespacedItems.unshift("index")
}
// Resolve non-namespaced items through the existing flow
// Get URLs for all registry items including their dependencies
const registryUrls: string[] = []
for (const name of nonNamespacedItems) {
const itemDependencies = await resolveRegistryDependencies(
name,
config,
options
)
registryUrls.push(...itemDependencies)
}
// Deduplicate URLs
const uniqueUrls = Array.from(new Set(registryUrls))
let result = await fetchRegistry(uniqueUrls, options)
const registryPayload = z.array(registryItemSchema).parse(result)
payload.push(...registryPayload)
}
}
const { items, registryNames } = await resolveDependenciesRecursively(
resolvedDependencies,
config,
options,
new Set(uniqueNames)
)
allDependencyItems.push(...items)
allDependencyRegistryNames.push(...registryNames)
}
}
if (!payload.length) {
return null
}
payload.push(...allDependencyItems)
// No deduplication - we want to support multiple items with the same name from different sources
// Handle any remaining registry names that need index resolution
if (allDependencyRegistryNames.length > 0) {
// Remove duplicates from registry names
const uniqueRegistryNames = Array.from(new Set(allDependencyRegistryNames))
// If we're resolving the index, we want to fetch
// the theme item if a base color is provided.
// We do this for index only.
// Other components will ship with their theme tokens.
if (
uniqueNames.includes("index") ||
allDependencyRegistryNames.includes("index")
) {
if (config.tailwind.baseColor) {
const theme = await registryGetTheme(config.tailwind.baseColor, config)
if (theme) {
payload.unshift(theme)
}
}
}
// Build source map for topological sort
const sourceMap = new Map<z.infer<typeof registryItemSchema>, string>()
payload.forEach((item) => {
// Use the _source property if it was added, otherwise use the name
const source = item._source || item.name
sourceMap.set(item, source)
})
// Apply topological sort to ensure dependencies come before dependents
payload = topologicalSortRegistryItems(payload, sourceMap)
// Sort the payload so that registry:theme items come first,
// while maintaining the relative order of all items.
payload.sort((a, b) => {
if (a.type === "registry:theme" && b.type !== "registry:theme") {
return -1
}
if (a.type !== "registry:theme" && b.type === "registry:theme") {
return 1
}
return 0
})
let tailwind = {}
payload.forEach((item) => {
tailwind = deepmerge(tailwind, item.tailwind ?? {})
})
let cssVars = {}
payload.forEach((item) => {
cssVars = deepmerge(cssVars, item.cssVars ?? {})
})
let css = {}
payload.forEach((item) => {
css = deepmerge(css, item.css ?? {})
})
let docs = ""
payload.forEach((item) => {
if (item.docs) {
docs += `${item.docs}\n`
}
})
let envVars = {}
payload.forEach((item) => {
envVars = deepmerge(envVars, item.envVars ?? {})
})
// Deduplicate files based on resolved target paths.
const deduplicatedFiles = await deduplicateFilesByTarget(
payload.map((item) => item.files ?? []),
config
// Separate namespaced and non-namespaced items
const nonNamespacedItems = uniqueRegistryNames.filter(
(name) => !name.startsWith("@")
)
const namespacedDepItems = uniqueRegistryNames.filter((name) =>
name.startsWith("@")
)
const parsed = registryResolvedItemsTreeSchema.parse({
dependencies: deepmerge.all(
payload.map((item) => item.dependencies ?? [])
),
devDependencies: deepmerge.all(
payload.map((item) => item.devDependencies ?? [])
),
files: deduplicatedFiles,
tailwind,
cssVars,
css,
docs,
})
// Handle namespaced dependency items
if (namespacedDepItems.length > 0) {
// This will now throw specific errors on failure
const depResults = await fetchRegistryItems(
namespacedDepItems,
config,
options
)
if (Object.keys(envVars).length > 0) {
parsed.envVars = envVars
for (let i = 0; i < depResults.length; i++) {
const item = depResults[i]
const itemWithSource: z.infer<typeof registryItemWithSourceSchema> = {
...item,
_source: namespacedDepItems[i],
}
payload.push(itemWithSource)
}
}
return parsed
} catch (error) {
handleError(error)
// For non-namespaced items, we need the index and style resolution
if (nonNamespacedItems.length > 0) {
const index = await getShadcnRegistryIndex()
if (!index && payload.length === 0) {
return null
}
if (index) {
// If we're resolving the index, we want it to go first
if (nonNamespacedItems.includes("index")) {
nonNamespacedItems.unshift("index")
}
// Resolve non-namespaced items through the existing flow
// Get URLs for all registry items including their dependencies
const registryUrls: string[] = []
for (const name of nonNamespacedItems) {
const itemDependencies = await resolveRegistryDependencies(
name,
config,
options
)
registryUrls.push(...itemDependencies)
}
// Deduplicate URLs
const uniqueUrls = Array.from(new Set(registryUrls))
let result = await fetchRegistry(uniqueUrls, options)
const registryPayload = z.array(registryItemSchema).parse(result)
payload.push(...registryPayload)
}
}
}
if (!payload.length) {
return null
}
// No deduplication - we want to support multiple items with the same name from different sources
// If we're resolving the index, we want to fetch
// the theme item if a base color is provided.
// We do this for index only.
// Other components will ship with their theme tokens.
if (
uniqueNames.includes("index") ||
allDependencyRegistryNames.includes("index")
) {
if (config.tailwind.baseColor) {
const theme = await registryGetTheme(config.tailwind.baseColor, config)
if (theme) {
payload.unshift(theme)
}
}
}
// Build source map for topological sort
const sourceMap = new Map<z.infer<typeof registryItemSchema>, string>()
payload.forEach((item) => {
// Use the _source property if it was added, otherwise use the name
const source = item._source || item.name
sourceMap.set(item, source)
})
// Apply topological sort to ensure dependencies come before dependents
payload = topologicalSortRegistryItems(payload, sourceMap)
// Sort the payload so that registry:theme items come first,
// while maintaining the relative order of all items.
payload.sort((a, b) => {
if (a.type === "registry:theme" && b.type !== "registry:theme") {
return -1
}
if (a.type !== "registry:theme" && b.type === "registry:theme") {
return 1
}
return 0
})
let tailwind = {}
payload.forEach((item) => {
tailwind = deepmerge(tailwind, item.tailwind ?? {})
})
let cssVars = {}
payload.forEach((item) => {
cssVars = deepmerge(cssVars, item.cssVars ?? {})
})
let css = {}
payload.forEach((item) => {
css = deepmerge(css, item.css ?? {})
})
let docs = ""
payload.forEach((item) => {
if (item.docs) {
docs += `${item.docs}\n`
}
})
let envVars = {}
payload.forEach((item) => {
envVars = deepmerge(envVars, item.envVars ?? {})
})
// Deduplicate files based on resolved target paths.
const deduplicatedFiles = await deduplicateFilesByTarget(
payload.map((item) => item.files ?? []),
config
)
const parsed = registryResolvedItemsTreeSchema.parse({
dependencies: deepmerge.all(payload.map((item) => item.dependencies ?? [])),
devDependencies: deepmerge.all(
payload.map((item) => item.devDependencies ?? [])
),
files: deduplicatedFiles,
tailwind,
cssVars,
css,
docs,
})
if (Object.keys(envVars).length > 0) {
parsed.envVars = envVars
}
return parsed
}
async function resolveDependenciesRecursively(

View File

@@ -1,5 +1,5 @@
import path from "path"
import { fetchRegistries } from "@/src/registry/api"
import { getRegistriesIndex } from "@/src/registry/api"
import { BUILTIN_REGISTRIES } from "@/src/registry/constants"
import { resolveRegistryNamespaces } from "@/src/registry/namespaces"
import { rawConfigSchema } from "@/src/registry/schema"
@@ -39,7 +39,10 @@ export async function ensureRegistriesInConfig(
// We'll fail silently if we can't fetch the registry index.
// The error handling by caller will guide user to add the missing registries.
const registryIndex = await fetchRegistries()
const registryIndex = await getRegistriesIndex({
useCache: process.env.NODE_ENV !== "development",
})
if (!registryIndex) {
return {
config,

View File

@@ -31,13 +31,10 @@ export const transformCssVars: Transformer = async ({
// }
// }
sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach((node) => {
const value = node.getText()
if (value) {
const valueWithColorMapping = applyColorMapping(
value.replace(/"/g, ""),
baseColor.inlineColors
)
node.replaceWithText(`"${valueWithColorMapping.trim()}"`)
const raw = node.getLiteralText()
const mapped = applyColorMapping(raw, baseColor.inlineColors).trim()
if (mapped !== raw) {
node.setLiteralValue(mapped)
}
})

View File

@@ -1,5 +1,6 @@
import { Config } from "@/src/utils/get-config"
import { Transformer } from "@/src/utils/transformers"
import { SyntaxKind } from "ts-morph"
export const transformImport: Transformer = async ({
sourceFile,
@@ -9,32 +10,34 @@ export const transformImport: Transformer = async ({
const workspaceAlias = config.aliases?.utils?.split("/")[0]?.slice(1)
const utilsImport = `@${workspaceAlias}/lib/utils`
const importDeclarations = sourceFile.getImportDeclarations()
if (![".tsx", ".ts", ".jsx", ".js"].includes(sourceFile.getExtension())) {
return sourceFile
}
for (const importDeclaration of importDeclarations) {
const moduleSpecifier = updateImportAliases(
importDeclaration.getModuleSpecifierValue(),
for (const specifier of sourceFile.getImportStringLiterals()) {
const updated = updateImportAliases(
specifier.getLiteralValue(),
config,
isRemote
)
importDeclaration.setModuleSpecifier(moduleSpecifier)
specifier.setLiteralValue(updated)
// Replace `import { cn } from "@/lib/utils"`
if (utilsImport === moduleSpecifier || moduleSpecifier === "@/lib/utils") {
const namedImports = importDeclaration.getNamedImports()
const cnImport = namedImports.find((i) => i.getName() === "cn")
if (cnImport) {
importDeclaration.setModuleSpecifier(
utilsImport === moduleSpecifier
? moduleSpecifier.replace(utilsImport, config.aliases.utils)
: config.aliases.utils
)
}
if (utilsImport === updated || updated === "@/lib/utils") {
const importDeclaration = specifier.getFirstAncestorByKind(
SyntaxKind.ImportDeclaration
)
const isCnImport = importDeclaration
?.getNamedImports()
.some((namedImport) => namedImport.getName() === "cn")
if (!isCnImport) continue
specifier.setLiteralValue(
utilsImport === updated
? updated.replace(utilsImport, config.aliases.utils)
: config.aliases.utils
)
}
}

View File

@@ -12,7 +12,7 @@ exports[`transform css vars 2`] = `
"import * as React from "react"
export function Foo() {
return <div className="bg-white hover:bg-stone-100 text-stone-50 sm:focus:text-stone-900 dark:bg-stone-950 dark:hover:bg-stone-800 dark:text-stone-900 dark:sm:focus:text-stone-50">foo</div>
}""
}"
"
`;
@@ -20,7 +20,7 @@ exports[`transform css vars 3`] = `
"import * as React from "react"
export function Foo() {
return <div className={cn("bg-white hover:bg-stone-100 dark:bg-stone-950 dark:hover:bg-stone-800", true && "text-stone-50 sm:focus:text-stone-900 dark:text-stone-900 dark:sm:focus:text-stone-50")}>foo</div>
}""
}"
"
`;
@@ -28,6 +28,6 @@ exports[`transform css vars 4`] = `
"import * as React from "react"
export function Foo() {
return <div className={cn("bg-white border border-stone-200 dark:bg-stone-950 dark:border-stone-800")}>foo</div>
}""
}"
"
`;

View File

@@ -1,5 +1,57 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`transform async/dynamic imports 1`] = `
"import * as React from "react"
import { Button } from "@/components/ui/button"
async function loadComponent() {
const { cn } = await import("@/lib/utils")
const module = await import("@/components/ui/card")
return module
}
function lazyLoad() {
return import("@/components/ui/dialog").then(module => module)
}
"
`;
exports[`transform async/dynamic imports 2`] = `
"import { Button } from "~/components/ui/button"
async function loadUtils() {
const utils = await import("~/lib/utils")
const { cn } = await import("~/lib/utils")
return { utils, cn }
}
const dialogPromise = import("~/components/ui/dialog")
const cardModule = import("~/components/ui/card")
"
`;
exports[`transform dynamic imports with cn utility 1`] = `
"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")
}
"
`;
exports[`transform dynamic imports with cn utility 2`] = `
"async function loadWorkspaceCn() {
const { cn } = await import("@workspace/lib/utils")
return cn
}
"
`;
exports[`transform import 1`] = `
"import * as React from "react"
import { Foo } from "bar"
@@ -91,3 +143,14 @@ import { Foo } from "bar"
import { cn } from "@repo/ui/lib/utils"
"
`;
exports[`transform re-exports with dynamic imports 1`] = `
"export { cn } from "@/lib/utils"
export { Button } from "@/components/ui/button"
async function load() {
const module = await import("@/components/ui/card")
return module
}
"
`;

View File

@@ -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-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>
}
"
`;

View File

@@ -144,7 +144,6 @@ import { Foo } from "bar"
).toMatchSnapshot()
})
test("transform import for monorepo", async () => {
expect(
await transform({
@@ -196,3 +195,122 @@ import { Foo } from "bar"
})
).toMatchSnapshot()
})
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("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()
})

View File

@@ -9,6 +9,7 @@ export default defineConfig({
"**/fixtures/**",
"**/templates/**",
],
testTimeout: 8000,
},
plugins: [
tsconfigPaths({

98
pnpm-lock.yaml generated
View File

@@ -325,7 +325,7 @@ importers:
specifier: ^6.0.1
version: 6.0.1
shadcn:
specifier: 3.2.1
specifier: 3.3.1
version: link:../../packages/shadcn
shiki:
specifier: ^1.10.1
@@ -605,7 +605,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: 3.2.1
specifier: 3.3.1
version: link:../../packages/shadcn
sharp:
specifier: ^0.32.6
@@ -713,12 +713,18 @@ importers:
'@babel/plugin-transform-typescript':
specifier: ^7.28.0
version: 7.28.0(@babel/core@7.28.0)
'@babel/preset-typescript':
specifier: ^7.27.1
version: 7.27.1(@babel/core@7.28.0)
'@dotenvx/dotenvx':
specifier: ^1.48.4
version: 1.48.4
'@modelcontextprotocol/sdk':
specifier: ^1.17.2
version: 1.17.2
browserslist:
specifier: ^4.26.2
version: 4.26.2
commander:
specifier: ^14.0.0
version: 14.0.0
@@ -943,18 +949,36 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-syntax-jsx@7.27.1':
resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-syntax-typescript@7.27.1':
resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-modules-commonjs@7.27.1':
resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/plugin-transform-typescript@7.28.0':
resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/preset-typescript@7.27.1':
resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
'@babel/runtime@7.28.2':
resolution: {integrity: sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==}
engines: {node: '>=6.9.0'}
@@ -4030,6 +4054,10 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.8.4:
resolution: {integrity: sha512-L+YvJwGAgwJBV1p6ffpSTa2KRc69EeeYGYjRVWKs0GKrK+LON0GC0gV+rKSNtALEDvMDqkvCFq9r1r94/Gjwxw==}
hasBin: true
basic-ftp@5.0.5:
resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==}
engines: {node: '>=10.0.0'}
@@ -4062,8 +4090,8 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
browserslist@4.25.2:
resolution: {integrity: sha512-0si2SJK3ooGzIawRu61ZdPCO1IncZwS8IzuX73sPZsXW6EQ/w/DAfPyKI8l1ETTCr2MnvqWitmlCUxgdul45jA==}
browserslist@4.26.2:
resolution: {integrity: sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
@@ -4138,6 +4166,9 @@ packages:
caniuse-lite@1.0.30001734:
resolution: {integrity: sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==}
caniuse-lite@1.0.30001743:
resolution: {integrity: sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -4697,8 +4728,8 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
electron-to-chromium@1.5.199:
resolution: {integrity: sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==}
electron-to-chromium@1.5.218:
resolution: {integrity: sha512-uwwdN0TUHs8u6iRgN8vKeWZMRll4gBkz+QMqdS7DDe49uiK68/UX92lFb61oiFPrpYZNeZIqa4bA7O6Aiasnzg==}
embla-carousel-autoplay@8.0.0-rc15:
resolution: {integrity: sha512-ABTbDJGNb9jzI9OV2vSpbUvxUA0ELmK0SI3yPm8Haj3ghssS+vElfahoDqp7zuFkWBRih6w3B51oMPKdF5J55A==}
@@ -6860,8 +6891,8 @@ packages:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-releases@2.0.19:
resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==}
node-releases@2.0.21:
resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}
normalize-package-data@2.5.0:
resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==}
@@ -9041,7 +9072,7 @@ snapshots:
dependencies:
'@babel/compat-data': 7.28.0
'@babel/helper-validator-option': 7.27.1
browserslist: 4.25.2
browserslist: 4.26.2
lru-cache: 5.1.1
semver: 6.3.1
@@ -9120,11 +9151,24 @@ snapshots:
dependencies:
'@babel/types': 7.28.2
'@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)':
dependencies:
'@babel/core': 7.28.0
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.0)':
dependencies:
'@babel/core': 7.28.0
'@babel/helper-plugin-utils': 7.27.1
'@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.0)':
dependencies:
'@babel/core': 7.28.0
'@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0)
'@babel/helper-plugin-utils': 7.27.1
transitivePeerDependencies:
- supports-color
'@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.0)':
dependencies:
'@babel/core': 7.28.0
@@ -9136,6 +9180,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@babel/preset-typescript@7.27.1(@babel/core@7.28.0)':
dependencies:
'@babel/core': 7.28.0
'@babel/helper-plugin-utils': 7.27.1
'@babel/helper-validator-option': 7.27.1
'@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0)
'@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.0)
'@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
transitivePeerDependencies:
- supports-color
'@babel/runtime@7.28.2': {}
'@babel/template@7.27.2':
@@ -12962,7 +13017,7 @@ snapshots:
autoprefixer@10.4.21(postcss@8.5.6):
dependencies:
browserslist: 4.25.2
browserslist: 4.26.2
caniuse-lite: 1.0.30001734
fraction.js: 4.3.7
normalize-range: 0.1.2
@@ -13019,6 +13074,8 @@ snapshots:
base64-js@1.5.1: {}
baseline-browser-mapping@2.8.4: {}
basic-ftp@5.0.5: {}
better-path-resolve@1.0.0:
@@ -13062,12 +13119,13 @@ snapshots:
dependencies:
fill-range: 7.1.1
browserslist@4.25.2:
browserslist@4.26.2:
dependencies:
caniuse-lite: 1.0.30001734
electron-to-chromium: 1.5.199
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.25.2)
baseline-browser-mapping: 2.8.4
caniuse-lite: 1.0.30001743
electron-to-chromium: 1.5.218
node-releases: 2.0.21
update-browserslist-db: 1.1.3(browserslist@4.26.2)
buffer-crc32@0.2.13: {}
@@ -13141,6 +13199,8 @@ snapshots:
caniuse-lite@1.0.30001734: {}
caniuse-lite@1.0.30001743: {}
ccount@2.0.1: {}
chai@5.2.1:
@@ -13660,7 +13720,7 @@ snapshots:
ee-first@1.1.1: {}
electron-to-chromium@1.5.199: {}
electron-to-chromium@1.5.218: {}
embla-carousel-autoplay@8.0.0-rc15(embla-carousel@8.0.0-rc15):
dependencies:
@@ -16674,7 +16734,7 @@ snapshots:
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-releases@2.0.19: {}
node-releases@2.0.21: {}
normalize-package-data@2.5.0:
dependencies:
@@ -18926,9 +18986,9 @@ snapshots:
'@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
'@unrs/resolver-binding-win32-x64-msvc': 1.11.1
update-browserslist-db@1.1.3(browserslist@4.25.2):
update-browserslist-db@1.1.3(browserslist@4.26.2):
dependencies:
browserslist: 4.25.2
browserslist: 4.26.2
escalade: 3.2.0
picocolors: 1.1.1