mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Only check selectors containing apply candidates for circular dependencies (#8222)
* Only check selectors containing base apply candidates for circular dependencies
When given a two rule like `html.dark .a, .b { … }` and `html.dark .c { @apply b }` we would see `.dark` in both the base rule and the rule being applied and consider it a circular dependency. However, the selectors `html.dark .a` and `.b` are considered on their own and is therefore do not introduce a circular dependency.
This better matches the user’s mental model that the selectors are just two definitions sharing the same properties.
* Update changelog
This commit is contained in:
parent
3989f77dd3
commit
7c337f24fc
@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Improve type detection for arbitrary color values ([#8201](https://github.com/tailwindlabs/tailwindcss/pull/8201))
|
||||
- Support PostCSS config options in config file in CLI ([#8226](https://github.com/tailwindlabs/tailwindcss/pull/8226))
|
||||
- Remove default `[hidden]` style in preflight ([#8248](https://github.com/tailwindlabs/tailwindcss/pull/8248))
|
||||
- Only check selectors containing base apply candidates for circular dependencies ([#8222](https://github.com/tailwindlabs/tailwindcss/pull/8222))
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
@ -8,18 +8,30 @@ import escapeClassName from '../util/escapeClassName'
|
||||
/** @typedef {Map<string, [any, import('postcss').Rule[]]>} ApplyCache */
|
||||
|
||||
function extractClasses(node) {
|
||||
let classes = new Set()
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
let groups = new Map()
|
||||
|
||||
let container = postcss.root({ nodes: [node.clone()] })
|
||||
|
||||
container.walkRules((rule) => {
|
||||
parser((selectors) => {
|
||||
selectors.walkClasses((classSelector) => {
|
||||
let parentSelector = classSelector.parent.toString()
|
||||
|
||||
let classes = groups.get(parentSelector)
|
||||
if (!classes) {
|
||||
groups.set(parentSelector, (classes = new Set()))
|
||||
}
|
||||
|
||||
classes.add(classSelector.value)
|
||||
})
|
||||
}).processSync(rule.selector)
|
||||
})
|
||||
|
||||
return Array.from(classes)
|
||||
let normalizedGroups = Array.from(groups.values(), (classes) => Array.from(classes))
|
||||
let classes = normalizedGroups.flat()
|
||||
|
||||
return Object.assign(classes, { groups: normalizedGroups })
|
||||
}
|
||||
|
||||
function extractBaseCandidates(candidates, separator) {
|
||||
@ -353,10 +365,23 @@ function processApply(root, context, localCache) {
|
||||
let siblings = []
|
||||
|
||||
for (let [applyCandidate, important, rules] of candidates) {
|
||||
let potentialApplyCandidates = [
|
||||
applyCandidate,
|
||||
...extractBaseCandidates([applyCandidate], context.tailwindConfig.separator),
|
||||
]
|
||||
|
||||
for (let [meta, node] of rules) {
|
||||
let parentClasses = extractClasses(parent)
|
||||
let nodeClasses = extractClasses(node)
|
||||
|
||||
// When we encounter a rule like `.dark .a, .b { … }` we only want to be left with `[.dark, .a]` if the base applyCandidate is `.a` or with `[.b]` if the base applyCandidate is `.b`
|
||||
// So we've split them into groups
|
||||
nodeClasses = nodeClasses.groups
|
||||
.filter((classList) =>
|
||||
classList.some((className) => potentialApplyCandidates.includes(className))
|
||||
)
|
||||
.flat()
|
||||
|
||||
// Add base utility classes from the @apply node to the list of
|
||||
// classes to check whether it intersects and therefore results in a
|
||||
// circular dependency or not.
|
||||
|
||||
@ -658,6 +658,94 @@ it('should throw when trying to apply an indirect circular dependency with a mod
|
||||
})
|
||||
})
|
||||
|
||||
it('should not throw when the circular dependency is part of a different selector (1)', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="c"></div>` }],
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
html.dark .a,
|
||||
.b {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .c {
|
||||
@apply b;
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
html.dark .c {
|
||||
color: red;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not throw when the circular dependency is part of a different selector (2)', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="c"></div>` }],
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
html.dark .a,
|
||||
.b {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .c {
|
||||
@apply hover:b;
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
html.dark .c:hover {
|
||||
color: red;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('should throw when the circular dependency is part of the same selector', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="c"></div>` }],
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
|
||||
@layer utilities {
|
||||
html.dark .a,
|
||||
html.dark .b {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
|
||||
html.dark .c {
|
||||
@apply hover:b;
|
||||
}
|
||||
`
|
||||
|
||||
return run(input, config).catch((err) => {
|
||||
expect(err.reason).toBe(
|
||||
'You cannot `@apply` the `hover:b` utility here because it creates a circular dependency.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('rules with vendor prefixes are still separate when optimizing defaults rules', () => {
|
||||
let config = {
|
||||
experimental: { optimizeUniversalDefaults: true },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user