From 5e004e744f0611fb8578d15cd2893ef2c44f03b9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 8 May 2025 13:12:39 +0200 Subject: [PATCH] Fix bug with nested @apply rules in utility classes (#17924) ## Bug When one utility applied another utility that contained pseudo-selectors (like :hover, :disabled), the pseudo-selectors were not properly carried through the dependency chain. This was because we were only tracking dependencies on the immediate parent rather than all parents in the path. ## Fix - Modified the dependency resolution to track dependencies through the entire parent path - Rewrote the node replacement logic to use the more robust walk() function - These changes ensure pseudo-selectors are properly preserved when utilities apply other utilities ## Test Plan - Run `vitest run packages/tailwindcss/src/index.test.ts` to verify the new test case passes - The test case verifies that when utility 'test2' applies utility 'test' with hover/disabled states, all pseudo-selectors are correctly preserved in the output CSS --- CHANGELOG.md | 1 + packages/tailwindcss/src/apply.ts | 21 ++++--- packages/tailwindcss/src/index.test.ts | 78 ++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be17ecc8e4b4..4437abaeb7aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix incorrectly replacing `_` with ` ` in arbitrary modifier shorthand `bg-red-500/(--my_opacity)` ([#17889](https://github.com/tailwindlabs/tailwindcss/pull/17889)) - Upgrade: Bump dependencies in parallel and make the upgrade faster ([#17898](https://github.com/tailwindlabs/tailwindcss/pull/17898)) - Don't scan `.log` files for classes by default ([#17906](https://github.com/tailwindlabs/tailwindcss/pull/17906)) +- Ensure that custom utilities applying other custom utilities don't swallow nested `@apply` rules ([#17925](https://github.com/tailwindlabs/tailwindcss/pull/17925)) ## [4.1.5] - 2025-04-30 diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 676b4f2eee19..a9316d184fd8 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -23,7 +23,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { let definitions = new DefaultMap(() => new Set()) // Collect all new `@utility` definitions and all `@apply` rules first - walk([root], (node, { parent }) => { + walk([root], (node, { parent, path }) => { if (node.kind !== 'at-rule') return // Do not allow `@apply` rules inside `@keyframes` rules. @@ -66,7 +66,12 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { parents.add(parent) for (let dependency of resolveApplyDependencies(node, designSystem)) { - dependencies.get(parent).add(dependency) + // Mark every parent in the path as having a dependency to that utility. + for (let parent of path) { + if (parent === node) continue + if (!parents.has(parent)) continue + dependencies.get(parent).add(dependency) + } } } }) @@ -151,11 +156,10 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { for (let parent of sorted) { if (!('nodes' in parent)) continue - for (let i = 0; i < parent.nodes.length; i++) { - let node = parent.nodes[i] - if (node.kind !== 'at-rule' || node.name !== '@apply') continue + walk(parent.nodes, (child, { replaceWith }) => { + if (child.kind !== 'at-rule' || child.name !== '@apply') return - let candidates = node.params.split(/\s+/g) + let candidates = child.params.split(/\s+/g) // Replace the `@apply` rule with the actual utility classes { @@ -181,10 +185,11 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) { } } - parent.nodes.splice(i, 1, ...newNodes) + replaceWith(newNodes) } - } + }) } + return features } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 00e73a90b405..9c3fd47ed5c6 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -612,6 +612,84 @@ describe('@apply', () => { }" `) }) + + // https://github.com/tailwindlabs/tailwindcss/issues/17924 + it('should correctly apply nested usages of @apply when one @utility applies another', async () => { + expect( + await compileCss( + css` + @theme { + --color-green-500: green; + --color-red-500: red; + --color-indigo-500: indigo; + } + + @tailwind utilities; + + @utility test2 { + @apply test; + } + + @utility test { + @apply bg-green-500; + &:hover { + @apply bg-red-500; + } + &:disabled { + @apply bg-indigo-500; + } + } + + .foo { + @apply test2; + } + `, + ['foo', 'test', 'test2'], + ), + ).toMatchInlineSnapshot(` + ":root, :host { + --color-green-500: green; + --color-red-500: red; + --color-indigo-500: indigo; + } + + .test { + background-color: var(--color-green-500); + } + + .test:hover { + background-color: var(--color-red-500); + } + + .test:disabled { + background-color: var(--color-indigo-500); + } + + .test2 { + background-color: var(--color-green-500); + } + + .test2:hover { + background-color: var(--color-red-500); + } + + .test2:disabled { + background-color: var(--color-indigo-500); + } + + .foo { + background-color: var(--color-green-500); + } + + .foo:hover { + background-color: var(--color-red-500); + } + + .foo:disabled { + background-color: var(--color-indigo-500); + }" + `) + }) }) describe('arbitrary variants', () => {