Skip to content

Commit db40530

Browse files
Don't swallow @utility declarations when @apply is used in nested rules (#16940)
Fixes #16935 This PR fixes an issue where the order of how `@apply` was resolved was incorrect for nested rules. Consider this example: ```css .rule { @apply underline; .nested-rule { @apply custom-utility; } } @Utility custom-utility { @apply flex; } ``` The way we topologically sort these, we end up with a list that looks roughly like this: ```css .rule { @apply underline; .nested-rule { @apply custom-utility; } } @Utility custom-utility { @apply flex; } .nested-rule { @apply custom-utility; } ``` As you can see here the nested rule is now part of the top-level list. This is correct because we first have to substitute the `@apply` inside the `@utility custom-utility` before we can apply the `custom-utility` inside `.nested-rule`. However, because we were using a regular AST walk and because the initial `.rule` also contains the `.nested-rule` as child, we would first substitute the `@apply` inside the `.nested-rule`, causing the design-system to force resolve (and cache) the wrong value for `custom-utility`. Because the list is already flattened, we do not need to recursively look into child declarations when we traverse the sorted list. This PR changes it to use a regular `for` loop instead of the `walk`. ## Test plan - Added a regression test - Rest of tests still green
1 parent b0aa20c commit db40530

File tree

3 files changed

+71
-29
lines changed

3 files changed

+71
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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

2121
- Ensure `not-*` does not remove `:is(…)` from variants ([#16825](https://github.com/tailwindlabs/tailwindcss/pull/16825))
2222
- Ensure `@keyframes` are correctly emitted when using a prefixed setup ([#16850](https://github.com/tailwindlabs/tailwindcss/pull/16850))
23+
- Don't swallow `@utility` declarations when `@apply` is used in nested rules ([#16940](https://github.com/tailwindlabs/tailwindcss/pull/16940))
2324
- Ensure `outline-hidden` behaves like `outline-none` in non-`forced-colors` mode ([#](https://github.com/tailwindlabs/tailwindcss/pull/))
2425

2526
## [4.0.9] - 2025-02-25

packages/tailwindcss/src/apply.ts

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -146,39 +146,45 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
146146
visit(node)
147147
}
148148

149-
// Substitute the `@apply` at-rules in order
150-
walk(sorted, (node, { replaceWith }) => {
151-
if (node.kind !== 'at-rule' || node.name !== '@apply') return
152-
let candidates = node.params.split(/\s+/g)
153-
154-
// Replace the `@apply` rule with the actual utility classes
155-
{
156-
// Parse the candidates to an AST that we can replace the `@apply` rule
157-
// with.
158-
let candidateAst = compileCandidates(candidates, designSystem, {
159-
onInvalidCandidate: (candidate) => {
160-
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
161-
},
162-
}).astNodes
163-
164-
// Collect the nodes to insert in place of the `@apply` rule. When a rule
165-
// was used, we want to insert its children instead of the rule because we
166-
// don't want the wrapping selector.
167-
let newNodes: AstNode[] = []
168-
for (let candidateNode of candidateAst) {
169-
if (candidateNode.kind === 'rule') {
170-
for (let child of candidateNode.nodes) {
171-
newNodes.push(child)
149+
// Substitute the `@apply` at-rules in order. Note that the list is going to
150+
// be flattened so we do not have to recursively walk over child rules
151+
for (let parent of sorted) {
152+
if (!('nodes' in parent)) continue
153+
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
157+
158+
let candidates = node.params.split(/\s+/g)
159+
160+
// Replace the `@apply` rule with the actual utility classes
161+
{
162+
// Parse the candidates to an AST that we can replace the `@apply` rule
163+
// with.
164+
let candidateAst = compileCandidates(candidates, designSystem, {
165+
onInvalidCandidate: (candidate) => {
166+
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
167+
},
168+
}).astNodes
169+
170+
// Collect the nodes to insert in place of the `@apply` rule. When a rule
171+
// was used, we want to insert its children instead of the rule because we
172+
// don't want the wrapping selector.
173+
let newNodes: AstNode[] = []
174+
for (let candidateNode of candidateAst) {
175+
if (candidateNode.kind === 'rule') {
176+
for (let child of candidateNode.nodes) {
177+
newNodes.push(child)
178+
}
179+
} else {
180+
newNodes.push(candidateNode)
172181
}
173-
} else {
174-
newNodes.push(candidateNode)
175182
}
176-
}
177183

178-
replaceWith(newNodes)
184+
parent.nodes.splice(i, 1, ...newNodes)
185+
}
179186
}
180-
})
181-
187+
}
182188
return features
183189
}
184190

packages/tailwindcss/src/index.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,41 @@ describe('@apply', () => {
530530
}"
531531
`)
532532
})
533+
534+
// https://github.com/tailwindlabs/tailwindcss/issues/16935
535+
it('should now swallow @utility declarations when @apply is used in nested rules', async () => {
536+
expect(
537+
await compileCss(
538+
css`
539+
@tailwind utilities;
540+
541+
.ignore-me {
542+
@apply underline;
543+
div {
544+
@apply custom-utility;
545+
}
546+
}
547+
548+
@utility custom-utility {
549+
@apply flex;
550+
}
551+
`,
552+
['custom-utility'],
553+
),
554+
).toMatchInlineSnapshot(`
555+
".custom-utility {
556+
display: flex;
557+
}
558+
559+
.ignore-me {
560+
text-decoration-line: underline;
561+
}
562+
563+
.ignore-me div {
564+
display: flex;
565+
}"
566+
`)
567+
})
533568
})
534569

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

0 commit comments

Comments
 (0)