Skip over classes inside :not(…) when nested in an at-rule (#12105)

* Skip over classes inside `:not(…)` when nested in an at-rule

When defining a utility we skip over classes inside `:not(…)` but we missed doing this when classes were contained within an at-rule. This fixes that.

* Update changelog
This commit is contained in:
Jordan Pittman 2023-09-28 10:45:59 -04:00
parent 666c7e4566
commit 3fa8ab1793
3 changed files with 129 additions and 32 deletions

View File

@ -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))
## [3.3.3] - 2023-07-13

View File

@ -148,43 +148,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()
}
})
}
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 classCandidate of classCandidates) {
classes.push(classCandidate)
}
}
// 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))
}
// 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 selector of 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 classCandidate of classCandidates) {
classes.push(classCandidate)
}
}
if (depth === 0) {

View File

@ -2,7 +2,7 @@ import fs from 'fs'
import path from 'path'
import { crosscheck, run, html, css, defaults } from './util/run'
crosscheck(({ stable, oxide }) => {
crosscheck(({ stable, oxide, engine }) => {
test('basic usage', () => {
let config = {
content: [
@ -1017,4 +1017,98 @@ crosscheck(({ stable, oxide }) => {
}
`)
})
test('detects quoted arbitrary values containing a slash', async () => {
let config = {
content: [
{
raw: html`<div class="group-[[href^='/']]:hidden"></div>`,
},
],
}
let input = css`
@tailwind utilities;
`
let result = await run(input, config)
expect(result.css).toMatchFormattedCss(
engine.oxide
? css`
.group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden {
display: none;
}
`
: css`
.hidden,
.group[href^='/'] .group-\[\[href\^\=\'\/\'\]\]\:hidden {
display: none;
}
`
)
})
test('handled quoted arbitrary values containing escaped spaces', async () => {
let config = {
content: [
{
raw: html`<div class="group-[[href^='_bar']]:hidden"></div>`,
},
],
}
let input = css`
@tailwind utilities;
`
let result = await run(input, config)
expect(result.css).toMatchFormattedCss(
engine.oxide
? css`
.group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden {
display: none;
}
`
: css`
.hidden,
.group[href^=' bar'] .group-\[\[href\^\=\'_bar\'\]\]\:hidden {
display: none;
}
`
)
})
test('Skips classes inside :not() when nested inside an at-rule', async () => {
let config = {
content: [
{
raw: html` <div class="disabled !disabled"></div> `,
},
],
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``)
})
})