fix: handling of apply directive inside utility

This commit is contained in:
shadcn
2026-02-21 21:41:01 +04:00
parent 9c99070d54
commit 40aca13fb0
2 changed files with 160 additions and 2 deletions

View File

@@ -317,7 +317,7 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
postcss.comment({ text: "---break---" })
)
// Add declarations with their values preserved
// Add declarations with their values preserved.
if (typeof properties === "object") {
for (const [prop, value] of Object.entries(properties)) {
if (typeof value === "string") {
@@ -327,13 +327,38 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
raws: { semicolon: true, before: "\n " },
})
atRule.append(decl)
} else if (
prop.startsWith("@") &&
typeof value === "object" &&
value !== null &&
Object.keys(value as Record<string, unknown>).length === 0
) {
// Handle at-rules with no body (e.g., @apply).
const atRuleMatch = prop.match(/@([a-zA-Z-]+)\s*(.*)/)
if (atRuleMatch) {
const [, atRuleName, atRuleParams] = atRuleMatch
const existingAtRule = atRule.nodes?.find(
(node): node is AtRule =>
node.type === "atrule" &&
node.name === atRuleName &&
node.params === atRuleParams
)
if (!existingAtRule) {
const newAtRule = postcss.atRule({
name: atRuleName,
params: atRuleParams,
raws: { semicolon: true, before: "\n " },
})
atRule.append(newAtRule)
}
}
} else if (typeof value === "object") {
processRule(atRule, prop, value)
}
}
}
} else {
// Update existing utility class
// Update existing utility class.
if (typeof properties === "object") {
for (const [prop, value] of Object.entries(properties)) {
if (typeof value === "string") {
@@ -351,6 +376,31 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
existingDecl
? existingDecl.replaceWith(decl)
: utilityAtRule.append(decl)
} else if (
prop.startsWith("@") &&
typeof value === "object" &&
value !== null &&
Object.keys(value as Record<string, unknown>).length === 0
) {
// Handle at-rules with no body (e.g., @apply).
const atRuleMatch = prop.match(/@([a-zA-Z-]+)\s*(.*)/)
if (atRuleMatch) {
const [, atRuleName, atRuleParams] = atRuleMatch
const existingAtRule = utilityAtRule.nodes?.find(
(node): node is AtRule =>
node.type === "atrule" &&
node.name === atRuleName &&
node.params === atRuleParams
)
if (!existingAtRule) {
const newAtRule = postcss.atRule({
name: atRuleName,
params: atRuleParams,
raws: { semicolon: true, before: "\n " },
})
utilityAtRule.append(newAtRule)
}
}
} else if (typeof value === "object") {
processRule(utilityAtRule, prop, value)
}

View File

@@ -929,6 +929,114 @@ describe("transformCss", () => {
`)
})
test("should handle @apply inside @utility", async () => {
const input = `@import "tailwindcss";`
const result = await transformCss(input, {
"@utility custom-btn": {
"@apply px-4 py-2 rounded-md": {},
},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@utility custom-btn {
@apply px-4 py-2 rounded-md;
}"
`)
})
test("should handle @apply mixed with declarations inside @utility", async () => {
const input = `@import "tailwindcss";`
const result = await transformCss(input, {
"@utility custom-card": {
"@apply bg-white shadow-md": {},
"border-radius": "0.5rem",
},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@utility custom-card {
@apply bg-white shadow-md;
border-radius: 0.5rem;
}"
`)
})
test("should handle multiple @apply inside @utility", async () => {
const input = `@import "tailwindcss";`
const result = await transformCss(input, {
"@utility custom-input": {
"@apply border border-gray-300 rounded-md": {},
"@apply focus:ring-2 focus:ring-blue-500": {},
padding: "0.5rem",
},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@utility custom-input {
@apply border border-gray-300 rounded-md;
@apply focus:ring-2 focus:ring-blue-500;
padding: 0.5rem;
}"
`)
})
test("should add @apply to existing @utility", async () => {
const input = `@import "tailwindcss";
@utility custom-alert {
font-size: 1rem;
}`
const result = await transformCss(input, {
"@utility custom-alert": {
"@apply font-bold text-red-500": {},
},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@utility custom-alert {
font-size: 1rem;
@apply font-bold text-red-500;
}"
`)
})
test("should not duplicate @apply inside existing @utility", async () => {
const input = `@import "tailwindcss";
@utility custom-badge {
@apply inline-flex items-center;
font-size: 0.75rem;
}`
const result = await transformCss(input, {
"@utility custom-badge": {
"@apply inline-flex items-center": {},
"font-size": "0.875rem",
},
})
expect(result).toMatchInlineSnapshot(`
"@import "tailwindcss";
@utility custom-badge {
@apply inline-flex items-center;
font-size: 0.875rem;
}"
`)
})
test("should replace existing keyframes instead of duplicating", async () => {
const input = `@import "tailwindcss";