diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f03cadaf409..f27cdddc8a95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Make `content` optional for presets in TypeScript types ([#11730](https://github.com/tailwindlabs/tailwindcss/pull/11730)) - Handle variable colors that have variable fallback values ([#12049](https://github.com/tailwindlabs/tailwindcss/pull/12049)) - Batch reading content files to prevent `too many open files` error ([#12079](https://github.com/tailwindlabs/tailwindcss/pull/12079)) +- Skip over classes inside `:not(…)` when nested in an at-rule ([#12105](https://github.com/tailwindlabs/tailwindcss/pull/12105)) ### Added diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js index 00b05652af0d..a81475654f7f 100644 --- a/src/lib/setupContextUtils.js +++ b/src/lib/setupContextUtils.js @@ -147,43 +147,45 @@ function getClasses(selector, mutate) { return parser.transformSync(selector) } +/** + * Ignore everything inside a :not(...). This allows you to write code like + * `div:not(.foo)`. If `.foo` is never found in your code, then we used to + * not generated it. But now we will ignore everything inside a `:not`, so + * that it still gets generated. + * + * @param {selectorParser.Root} selectors + */ +function ignoreNot(selectors) { + selectors.walkPseudos((pseudo) => { + if (pseudo.value === ':not') { + pseudo.remove() + } + }) +} + function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) { let classes = [] + let selectors = [] - // Handle normal rules if (node.type === 'rule') { - // Ignore everything inside a :not(...). This allows you to write code like - // `div:not(.foo)`. If `.foo` is never found in your code, then we used to - // not generated it. But now we will ignore everything inside a `:not`, so - // that it still gets generated. - function ignoreNot(selectors) { - selectors.walkPseudos((pseudo) => { - if (pseudo.value === ':not') { - pseudo.remove() - } - }) - } + // Handle normal rules + selectors.push(...node.selectors) + } else if (node.type === 'atrule') { + // Handle at-rules (which contains nested rules) + node.walkRules((rule) => selectors.push(...rule.selectors)) + } - for (let selector of node.selectors) { - let classCandidates = getClasses(selector, ignoreNot) - // At least one of the selectors contains non-"on-demandable" candidates. - if (classCandidates.length === 0) { - state.containsNonOnDemandable = true - } + for (let selector of selectors) { + let classCandidates = getClasses(selector, ignoreNot) - for (let classCandidate of classCandidates) { - classes.push(classCandidate) - } + // At least one of the selectors contains non-"on-demandable" candidates. + if (classCandidates.length === 0) { + state.containsNonOnDemandable = true } - } - // Handle at-rules (which contains nested rules) - else if (node.type === 'atrule') { - node.walkRules((rule) => { - for (let classCandidate of rule.selectors.flatMap((selector) => getClasses(selector))) { - classes.push(classCandidate) - } - }) + for (let classCandidate of classCandidates) { + classes.push(classCandidate) + } } if (depth === 0) { diff --git a/tests/basic-usage.test.js b/tests/basic-usage.test.js index 393db444d5bb..2ac46990b972 100644 --- a/tests/basic-usage.test.js +++ b/tests/basic-usage.test.js @@ -760,3 +760,35 @@ test('handled quoted arbitrary values containing escaped spaces', async () => { ` ) }) + +test('Skips classes inside :not() when nested inside an at-rule', async () => { + let config = { + content: [ + { + raw: html`
`, + }, + ], + corePlugins: { preflight: false }, + plugins: [ + function ({ addUtilities }) { + addUtilities({ + '.hand:not(.disabled)': { + '@supports (cursor: pointer)': { + cursor: 'pointer', + }, + }, + }) + }, + ], + } + + 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``) +})