diff --git a/CHANGELOG.md b/CHANGELOG.md index dc7ae82330e8..3ad1df938f5f 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 6a1fcdefe57c..676b4f2eee19 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) - - // 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) + // 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 + + for (let i = 0; i < parent.nodes.length; i++) { + let node = parent.nodes[i] + if (node.kind !== 'at-rule' || node.name !== '@apply') continue + + 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) } - } - replaceWith(newNodes) + parent.nodes.splice(i, 1, ...newNodes) + } } - }) - + } return features } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index ea07499b61ff..4c97475a3c20 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', () => {