From 17ca56d38652b7d84490c426bb47c345ed6cbeb9 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Thu, 8 May 2025 18:26:07 +0200 Subject: [PATCH] Fix bug with nested @apply rules in utility classes (#17924) (#17925) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #17924 When an `@apply` pointed to utility that nested usages of `@apply`, these nested usages were not properly carried through the dependency chain. This was because we were only tracking dependencies on the immediate parent rather than all parents. To fix this, this PR: - Modifies the dependency resolution to track dependencies through the entire parent path - Uses a `walk(…)` for the node replacement logic so that all nested `@apply` usages are also resolved (as these are now tracked in the dependency list anyways ## Test Plan - Added a regression test for #17924 to the unit tests and ensure existing tests don't break --- 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 be17ecc8e..4437abaeb 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 676b4f2ee..a9316d184 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 00e73a90b..9c3fd47ed 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', () => {