From 79f21a8f4575f70648428f1f6e98f48ae57adcbc Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 21 Jan 2025 10:38:32 -0500 Subject: [PATCH] Prevent `not-*` from being used with variants with multiple sibling rules (#15689) Variants like this can't be easily negated by our current system: ```css @custom-variant dark { &.is-dark { @slot; } @media (prefers-color-scheme: dark) { @slot; } } ``` Right now it produces the following CSS which is logically incorrect: ```css .utility-name { &:not(.is-dark) { /* ... */ } @media not (prefers-color-scheme: dark) { /* ... */ } } ``` The correct CSS is this which requires moving things around: ```css .utility-name { @media not (prefers-color-scheme: dark) { &:not(.is-dark) { /* ... */ } } } ``` We're opting to disable this instead of generating incorrect CSS for now. I'd like to bring this back in the future for simpler cases in the future. --- CHANGELOG.md | 1 + packages/tailwindcss/src/compile.ts | 9 ++++ packages/tailwindcss/src/variants.test.ts | 50 ++++++++++++++++++----- 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ad1a59c..5395c16dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Ensure `-outline-offset-*` utilities are suggested in IntelliSense ([#15646](https://github.com/tailwindlabs/tailwindcss/pull/15646)) - Write to `stdout` when `--output` is set to `-` or omitted for `@tailwindcss/cli` ([#15656](https://github.com/tailwindlabs/tailwindcss/pull/15656)) - Write to `stdout` when `--output -` flag is used for `@tailwindcss/cli` ([#15656](https://github.com/tailwindlabs/tailwindcss/pull/15656)) +- Prevent `not-*` from being used with variants that have multiple sibling rules ([#15689](https://github.com/tailwindlabs/tailwindcss/pull/15689)) - _Upgrade (experimental)_: Pretty print `--spacing(…)` to prevent ambiguity ([#15596](https://github.com/tailwindlabs/tailwindcss/pull/15596)) ### Changed diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index 2275971fb..5d7e82f73 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -219,6 +219,15 @@ export function applyVariant( let result = applyVariant(isolatedNode, variant.variant, variants, depth + 1) if (result === null) return null + if (variant.root === 'not' && isolatedNode.nodes.length > 1) { + // The `not` variant cannot negate sibling rules / at-rules because these + // are an OR relationship. Doing so would require transforming sibling + // nodes into nesting while negating them. This isn't possible with the + // current implementation of the `not` variant or with how variants are + // applied in general (on a per-node basis). + return null + } + for (let child of isolatedNode.nodes) { // Only some variants wrap children in rules. For example, the `force` // variant is a noop on the AST. And the `has` variant modifies the diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 5f4b9d106..e70970a92 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -1644,6 +1644,30 @@ test('not', async () => { } } } + @custom-variant parallel-style-rules { + &:hover { + @slot; + } + &:focus { + @slot; + } + } + @custom-variant parallel-at-rules { + @media foo { + @slot; + } + @media bar { + @slot; + } + } + @custom-variant parallel-mixed-rules { + &:hover { + @slot; + } + @media bar { + @slot; + } + } @tailwind utilities; `, [ @@ -1660,23 +1684,27 @@ test('not', async () => { 'not-multiple-media-conditions:flex', 'not-starting:flex', + 'not-parallel-style-rules:flex', + 'not-parallel-at-rules:flex', + 'not-parallel-mixed-rules:flex', + // The following built-in variants don't have not-* versions because // there is no sensible negative version of them. // These just don't make sense as not-* - 'not-force', - 'not-*', + 'not-force:flex', + 'not-*:flex', // These contain pseudo-elements - 'not-first-letter', - 'not-first-line', - 'not-marker', - 'not-selection', - 'not-file', - 'not-placeholder', - 'not-backdrop', - 'not-before', - 'not-after', + 'not-first-letter:flex', + 'not-first-line:flex', + 'not-marker:flex', + 'not-selection:flex', + 'not-file:flex', + 'not-placeholder:flex', + 'not-backdrop:flex', + 'not-before:flex', + 'not-after:flex', // This is not a conditional at rule 'not-starting:flex',