diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4fb368fc38..67deb410e402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Skip over classes inside `:not(…)` when nested in an at-rule ([#12105](https://github.com/tailwindlabs/tailwindcss/pull/12105)) - Update types to work with `Node16` module resolution ([#12097](https://github.com/tailwindlabs/tailwindcss/pull/12097)) - Don’t crash when important and parent selectors are equal in `@apply` ([#12112](https://github.com/tailwindlabs/tailwindcss/pull/12112)) +- Eliminate irrelevant rules when applying variants ([#12113](https://github.com/tailwindlabs/tailwindcss/pull/12113)) ### Added diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js index a3418d6c6f25..da893f21a085 100644 --- a/src/lib/generateRules.js +++ b/src/lib/generateRules.js @@ -815,10 +815,19 @@ function applyFinalFormat(match, { context, candidate }) { } try { - rule.selector = finalizeSelector(rule.selector, finalFormat, { + let selector = finalizeSelector(rule.selector, finalFormat, { candidate, context, }) + + // Finalize Selector determined that this candidate is irrelevant + // TODO: This elimination should happen earlier so this never happens + if (selector === null) { + rule.remove() + return + } + + rule.selector = selector } catch { // If this selector is invalid we also want to skip it // But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content diff --git a/src/util/formatVariantSelector.js b/src/util/formatVariantSelector.js index bf8076183655..6ba6f2cb2b43 100644 --- a/src/util/formatVariantSelector.js +++ b/src/util/formatVariantSelector.js @@ -186,6 +186,13 @@ export function finalizeSelector(current, formats, { context, candidate, base }) // Remove extraneous selectors that do not include the base candidate selector.each((sel) => eliminateIrrelevantSelectors(sel, base)) + // If ffter eliminating irrelevant selectors, we end up with nothing + // Then the whole "rule" this is associated with does not need to exist + // We use `null` as a marker value for that case + if (selector.length === 0) { + return null + } + // If there are no formats that means there were no variants added to the candidate // so we can just return the selector as-is let formatAst = Array.isArray(formats) diff --git a/tests/basic-usage.test.js b/tests/basic-usage.test.js index 2ac46990b972..1cc249d965fc 100644 --- a/tests/basic-usage.test.js +++ b/tests/basic-usage.test.js @@ -792,3 +792,46 @@ test('Skips classes inside :not() when nested inside an at-rule', async () => { expect(result.css).toMatchFormattedCss(css``) }) + +test('Irrelevant rules are removed when applying variants', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + function ({ addUtilities }) { + addUtilities({ + '@supports (foo: bar)': { + // This doesn't contain `w-full` so it should not exist in the output + '.outer': { color: 'red' }, + '.outer:is(.w-full)': { color: 'green' }, + }, + }) + }, + ], + } + + let input = css` + @tailwind utilities; + ` + + // We didn't find the hand class therefore + // nothing should be generated + let result = await run(input, config) + + expect(result.css).toMatchFormattedCss(css` + @media (min-width: 768px) { + .md\:w-full { + width: 100%; + } + @supports (foo: bar) { + .outer.md\:w-full { + color: green; + } + } + } + `) +})