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
This commit is contained in:
Philipp Spiess 2025-03-04 16:54:05 +01:00 committed by GitHub
parent b0aa20c30e
commit db405304f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 69 additions and 27 deletions

View File

@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Ensure `not-*` does not remove `:is(…)` from variants ([#16825](https://github.com/tailwindlabs/tailwindcss/pull/16825))
- Ensure `@keyframes` are correctly emitted when using a prefixed setup ([#16850](https://github.com/tailwindlabs/tailwindcss/pull/16850))
- Don't swallow `@utility` declarations when `@apply` is used in nested rules ([#16940](https://github.com/tailwindlabs/tailwindcss/pull/16940))
- Ensure `outline-hidden` behaves like `outline-none` in non-`forced-colors` mode ([#](https://github.com/tailwindlabs/tailwindcss/pull/))
## [4.0.9] - 2025-02-25

View File

@ -146,39 +146,45 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
visit(node)
}
// Substitute the `@apply` at-rules in order
walk(sorted, (node, { replaceWith }) => {
if (node.kind !== 'at-rule' || node.name !== '@apply') return
let candidates = node.params.split(/\s+/g)
// Substitute the `@apply` at-rules in order. Note that the list is going to
// be flattened so we do not have to recursively walk over child rules
for (let parent of sorted) {
if (!('nodes' in parent)) continue
// Replace the `@apply` rule with the actual utility classes
{
// Parse the candidates to an AST that we can replace the `@apply` rule
// with.
let candidateAst = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
},
}).astNodes
for (let i = 0; i < parent.nodes.length; i++) {
let node = parent.nodes[i]
if (node.kind !== 'at-rule' || node.name !== '@apply') continue
// Collect the nodes to insert in place of the `@apply` rule. When a rule
// was used, we want to insert its children instead of the rule because we
// don't want the wrapping selector.
let newNodes: AstNode[] = []
for (let candidateNode of candidateAst) {
if (candidateNode.kind === 'rule') {
for (let child of candidateNode.nodes) {
newNodes.push(child)
let candidates = node.params.split(/\s+/g)
// Replace the `@apply` rule with the actual utility classes
{
// Parse the candidates to an AST that we can replace the `@apply` rule
// with.
let candidateAst = compileCandidates(candidates, designSystem, {
onInvalidCandidate: (candidate) => {
throw new Error(`Cannot apply unknown utility class: ${candidate}`)
},
}).astNodes
// Collect the nodes to insert in place of the `@apply` rule. When a rule
// was used, we want to insert its children instead of the rule because we
// don't want the wrapping selector.
let newNodes: AstNode[] = []
for (let candidateNode of candidateAst) {
if (candidateNode.kind === 'rule') {
for (let child of candidateNode.nodes) {
newNodes.push(child)
}
} else {
newNodes.push(candidateNode)
}
} else {
newNodes.push(candidateNode)
}
parent.nodes.splice(i, 1, ...newNodes)
}
replaceWith(newNodes)
}
})
}
return features
}

View File

@ -530,6 +530,41 @@ describe('@apply', () => {
}"
`)
})
// https://github.com/tailwindlabs/tailwindcss/issues/16935
it('should now swallow @utility declarations when @apply is used in nested rules', async () => {
expect(
await compileCss(
css`
@tailwind utilities;
.ignore-me {
@apply underline;
div {
@apply custom-utility;
}
}
@utility custom-utility {
@apply flex;
}
`,
['custom-utility'],
),
).toMatchInlineSnapshot(`
".custom-utility {
display: flex;
}
.ignore-me {
text-decoration-line: underline;
}
.ignore-me div {
display: flex;
}"
`)
})
})
describe('arbitrary variants', () => {