mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-07-01 00:24:20 +00:00
Compare commits
1 Commits
@shadcn/re
...
fix/cn-cla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5be151c6d9 |
5
.changeset/cold-dancers-fix.md
Normal file
5
.changeset/cold-dancers-fix.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"shadcn": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
handle cn-\* class transformation in non-className contexts
|
||||||
@@ -1027,4 +1027,162 @@ 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",
|
||||||
|
}
|
||||||
|
"
|
||||||
|
`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const transformStyleMap: TransformerStyle<SourceFile> = async ({
|
|||||||
applyToCvaCalls(sourceFile, styleMap, matchedClasses)
|
applyToCvaCalls(sourceFile, styleMap, matchedClasses)
|
||||||
applyToClassNameAttributes(sourceFile, styleMap, matchedClasses)
|
applyToClassNameAttributes(sourceFile, styleMap, matchedClasses)
|
||||||
applyToMergePropsCalls(sourceFile, styleMap, matchedClasses)
|
applyToMergePropsCalls(sourceFile, styleMap, matchedClasses)
|
||||||
|
applyToAllStringLiterals(sourceFile, styleMap)
|
||||||
|
|
||||||
return sourceFile
|
return sourceFile
|
||||||
}
|
}
|
||||||
@@ -288,11 +289,6 @@ function extractCnClasses(str: string) {
|
|||||||
return Array.from(matches, (match) => match[0])
|
return Array.from(matches, (match) => match[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractCnClass(str: string) {
|
|
||||||
const classes = extractCnClasses(str)
|
|
||||||
return classes[0] ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeCnClasses(str: string) {
|
function removeCnClasses(str: string) {
|
||||||
return str
|
return str
|
||||||
.replace(/\bcn-[\w-]+\b/g, (match) => {
|
.replace(/\bcn-[\w-]+\b/g, (match) => {
|
||||||
@@ -606,3 +602,69 @@ function applyClassesToCnCall(
|
|||||||
cnCall.replaceWithText(`cn(${updatedArguments.join(", ")})`)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user