Compare commits

..

1 Commits

Author SHA1 Message Date
shadcn
505298ca0f deps: next canary 2026-05-08 09:03:06 +04:00
6 changed files with 809 additions and 1034 deletions

View File

@@ -1,5 +0,0 @@
---
"shadcn": patch
---
handle cn-\* class transformation in non-className contexts

View File

@@ -60,14 +60,14 @@
"lru-cache": "^11.2.4",
"lucide-react": "0.474.0",
"motion": "^12.12.1",
"next": "16.1.6",
"next": "16.3.0-canary.16",
"next-themes": "0.4.6",
"nuqs": "^2.8.9",
"postcss": "^8.5.1",
"radix-ui": "^1.4.3",
"react": "19.2.3",
"react": "19.2.6",
"react-day-picker": "^9.7.0",
"react-dom": "19.2.3",
"react-dom": "19.2.6",
"react-hook-form": "^7.62.0",
"react-qr-code": "^2.0.18",
"react-resizable-panels": "^4",

View File

@@ -522,7 +522,7 @@
"name": "@ncdai",
"homepage": "https://chanhdai.com/components",
"url": "https://chanhdai.com/r/{name}.json",
"description": "Pixel-perfect, uniquely crafted.",
"description": "A collection of reusable components.",
"logo": "<svg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 640 640'><path fill='var(--foreground)' d='M0 0h640v640H0z'/><path fill='var(--background)' d='M256 448H128v-64h128v64ZM512 256H384v128h128v64H320V192h192v64ZM128 384H64V256h64v128ZM576 384h-64V256h64v128ZM256 256H128v-64h128v64Z'/></svg>"
},
{

View File

@@ -1027,162 +1027,4 @@ function Menu({ className, ...props }: React.ComponentProps<"div">) {
"
`)
})
it("applies styles to cn-* classes in object properties (toastOptions pattern)", async () => {
const source = `import * as React from "react"
import { Toaster as Sonner } from "sonner"
const Toaster = ({ ...props }) => {
return (
<Sonner
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
)
}
`
const styleMap: StyleMap = {
"cn-toast": "rounded-2xl",
}
const result = await applyTransform(source, styleMap)
expect(result).toMatchInlineSnapshot(`
"import * as React from "react"
import { Toaster as Sonner } from "sonner"
const Toaster = ({ ...props }) => {
return (
<Sonner
toastOptions={{
classNames: {
toast: "rounded-2xl",
},
}}
{...props}
/>
)
}
"
`)
})
it("applies styles to cn-* classes in deeply nested object properties", async () => {
const source = `import * as React from "react"
const config = {
options: {
classNames: {
wrapper: "cn-wrapper existing-class",
inner: "cn-inner",
},
},
}
`
const styleMap: StyleMap = {
"cn-wrapper": "flex flex-col",
"cn-inner": "p-4 rounded-lg",
}
const result = await applyTransform(source, styleMap)
expect(result).toMatchInlineSnapshot(`
"import * as React from "react"
const config = {
options: {
classNames: {
wrapper: "flex flex-col existing-class",
inner: "p-4 rounded-lg",
},
},
}
"
`)
})
it("removes cn-* classes from object properties when not in styleMap", async () => {
const source = `import * as React from "react"
const config = {
classNames: {
item: "cn-unknown-class existing-class",
},
}
`
const styleMap: StyleMap = {}
const result = await applyTransform(source, styleMap)
expect(result).toMatchInlineSnapshot(`
"import * as React from "react"
const config = {
classNames: {
item: "existing-class",
},
}
"
`)
})
it("preserves allowlisted cn-* classes in object properties", async () => {
const source = `import * as React from "react"
const config = {
classNames: {
target: "cn-menu-target",
},
}
`
const styleMap: StyleMap = {
"cn-menu-target": "z-50 origin-top",
}
const result = await applyTransform(source, styleMap)
expect(result).toMatchInlineSnapshot(`
"import * as React from "react"
const config = {
classNames: {
target: "cn-menu-target",
},
}
"
`)
})
it("handles multiple cn-* classes in object property values", async () => {
const source = `import * as React from "react"
const options = {
toast: "cn-toast cn-toast-content extra-class",
}
`
const styleMap: StyleMap = {
"cn-toast": "rounded-lg",
"cn-toast-content": "p-4",
}
const result = await applyTransform(source, styleMap)
expect(result).toMatchInlineSnapshot(`
"import * as React from "react"
const options = {
toast: "rounded-lg p-4 extra-class",
}
"
`)
})
})

View File

@@ -41,7 +41,6 @@ export const transformStyleMap: TransformerStyle<SourceFile> = async ({
applyToCvaCalls(sourceFile, styleMap, matchedClasses)
applyToClassNameAttributes(sourceFile, styleMap, matchedClasses)
applyToMergePropsCalls(sourceFile, styleMap, matchedClasses)
applyToAllStringLiterals(sourceFile, styleMap)
return sourceFile
}
@@ -289,6 +288,11 @@ function extractCnClasses(str: string) {
return Array.from(matches, (match) => match[0])
}
function extractCnClass(str: string) {
const classes = extractCnClasses(str)
return classes[0] ?? null
}
function removeCnClasses(str: string) {
return str
.replace(/\bcn-[\w-]+\b/g, (match) => {
@@ -602,69 +606,3 @@ function applyClassesToCnCall(
cnCall.replaceWithText(`cn(${updatedArguments.join(", ")})`)
}
}
function isInAlreadyProcessedContext(node: Node): boolean {
let current = node.getParent()
while (current) {
if (Node.isCallExpression(current)) {
const expression = current.getExpression()
if (Node.isIdentifier(expression)) {
const name = expression.getText()
if (name === "cn" || name === "cva") {
return true
}
}
}
if (Node.isJsxAttribute(current)) {
const attrName = current.getNameNode().getText()
if (attrName === "className") {
return true
}
}
current = current.getParent()
}
return false
}
function applyToAllStringLiterals(sourceFile: SourceFile, styleMap: StyleMap) {
sourceFile.forEachDescendant((node) => {
if (!isStringLiteralLike(node)) {
return
}
if (isInAlreadyProcessedContext(node)) {
return
}
const stringValue = node.getLiteralText()
const cnClasses = extractCnClasses(stringValue)
if (cnClasses.length === 0) {
return
}
// Skip allowlisted classes — they are handled at CLI install time.
const classesToInline = cnClasses.filter(
(cnClass) => !ALLOWLIST.has(cnClass)
)
const tailwindClassesToApply = classesToInline
.map((cnClass) => styleMap[cnClass])
.filter((classes): classes is string => Boolean(classes))
if (tailwindClassesToApply.length > 0) {
const mergedClasses = tailwindClassesToApply.join(" ")
const updated = removeCnClasses(mergeClasses(mergedClasses, stringValue))
node.setLiteralValue(updated)
} else {
const updated = removeCnClasses(stringValue)
if (updated !== stringValue) {
node.setLiteralValue(updated)
}
}
})
}

1600
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff