Skip to content

Commit 17ca56d

Browse files
Fix bug with nested @apply rules in utility classes (#17924) (#17925)
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
1 parent 179e5dd commit 17ca56d

File tree

3 files changed

+92
-8
lines changed

3 files changed

+92
-8
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Fix incorrectly replacing `_` with ` ` in arbitrary modifier shorthand `bg-red-500/(--my_opacity)` ([#17889](https://github.com/tailwindlabs/tailwindcss/pull/17889))
2121
- Upgrade: Bump dependencies in parallel and make the upgrade faster ([#17898](https://github.com/tailwindlabs/tailwindcss/pull/17898))
2222
- Don't scan `.log` files for classes by default ([#17906](https://github.com/tailwindlabs/tailwindcss/pull/17906))
23+
- Ensure that custom utilities applying other custom utilities don't swallow nested `@apply` rules ([#17925](https://github.com/tailwindlabs/tailwindcss/pull/17925))
2324

2425
## [4.1.5] - 2025-04-30
2526

packages/tailwindcss/src/apply.ts

+13-8
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
2323
let definitions = new DefaultMap(() => new Set<AstNode>())
2424

2525
// Collect all new `@utility` definitions and all `@apply` rules first
26-
walk([root], (node, { parent }) => {
26+
walk([root], (node, { parent, path }) => {
2727
if (node.kind !== 'at-rule') return
2828

2929
// Do not allow `@apply` rules inside `@keyframes` rules.
@@ -66,7 +66,12 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
6666
parents.add(parent)
6767

6868
for (let dependency of resolveApplyDependencies(node, designSystem)) {
69-
dependencies.get(parent).add(dependency)
69+
// Mark every parent in the path as having a dependency to that utility.
70+
for (let parent of path) {
71+
if (parent === node) continue
72+
if (!parents.has(parent)) continue
73+
dependencies.get(parent).add(dependency)
74+
}
7075
}
7176
}
7277
})
@@ -151,11 +156,10 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
151156
for (let parent of sorted) {
152157
if (!('nodes' in parent)) continue
153158

154-
for (let i = 0; i < parent.nodes.length; i++) {
155-
let node = parent.nodes[i]
156-
if (node.kind !== 'at-rule' || node.name !== '@apply') continue
159+
walk(parent.nodes, (child, { replaceWith }) => {
160+
if (child.kind !== 'at-rule' || child.name !== '@apply') return
157161

158-
let candidates = node.params.split(/\s+/g)
162+
let candidates = child.params.split(/\s+/g)
159163

160164
// Replace the `@apply` rule with the actual utility classes
161165
{
@@ -181,10 +185,11 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
181185
}
182186
}
183187

184-
parent.nodes.splice(i, 1, ...newNodes)
188+
replaceWith(newNodes)
185189
}
186-
}
190+
})
187191
}
192+
188193
return features
189194
}
190195

packages/tailwindcss/src/index.test.ts

+78
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,84 @@ describe('@apply', () => {
612612
}"
613613
`)
614614
})
615+
616+
// https://github.com/tailwindlabs/tailwindcss/issues/17924
617+
it('should correctly apply nested usages of @apply when one @utility applies another', async () => {
618+
expect(
619+
await compileCss(
620+
css`
621+
@theme {
622+
--color-green-500: green;
623+
--color-red-500: red;
624+
--color-indigo-500: indigo;
625+
}
626+
627+
@tailwind utilities;
628+
629+
@utility test2 {
630+
@apply test;
631+
}
632+
633+
@utility test {
634+
@apply bg-green-500;
635+
&:hover {
636+
@apply bg-red-500;
637+
}
638+
&:disabled {
639+
@apply bg-indigo-500;
640+
}
641+
}
642+
643+
.foo {
644+
@apply test2;
645+
}
646+
`,
647+
['foo', 'test', 'test2'],
648+
),
649+
).toMatchInlineSnapshot(`
650+
":root, :host {
651+
--color-green-500: green;
652+
--color-red-500: red;
653+
--color-indigo-500: indigo;
654+
}
655+
656+
.test {
657+
background-color: var(--color-green-500);
658+
}
659+
660+
.test:hover {
661+
background-color: var(--color-red-500);
662+
}
663+
664+
.test:disabled {
665+
background-color: var(--color-indigo-500);
666+
}
667+
668+
.test2 {
669+
background-color: var(--color-green-500);
670+
}
671+
672+
.test2:hover {
673+
background-color: var(--color-red-500);
674+
}
675+
676+
.test2:disabled {
677+
background-color: var(--color-indigo-500);
678+
}
679+
680+
.foo {
681+
background-color: var(--color-green-500);
682+
}
683+
684+
.foo:hover {
685+
background-color: var(--color-red-500);
686+
}
687+
688+
.foo:disabled {
689+
background-color: var(--color-indigo-500);
690+
}"
691+
`)
692+
})
615693
})
616694

617695
describe('arbitrary variants', () => {

0 commit comments

Comments
 (0)