From db405304f40f85629ae8e2cff53eb88054939415 Mon Sep 17 00:00:00 2001 From: Philipp Spiess Date: Tue, 4 Mar 2025 16:54:05 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + packages/tailwindcss/src/apply.ts | 60 ++++++++++++++------------ packages/tailwindcss/src/index.test.ts | 35 +++++++++++++++ 3 files changed, 69 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc7ae8233..3ad1df938 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 - 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 diff --git a/packages/tailwindcss/src/apply.ts b/packages/tailwindcss/src/apply.ts index 6a1fcdefe..676b4f2ee 100644 --- a/packages/tailwindcss/src/apply.ts +++ b/packages/tailwindcss/src/apply.ts @@ -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 } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index ea07499b6..4c97475a3 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -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', () => {