diff --git a/CHANGELOG.md b/CHANGELOG.md index 1321211f39ee..f7db3ec0e243 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js index 65fe7950f0a1..0210684c8f00 100644 --- a/src/lib/expandApplyAtRules.js +++ b/src/lib/expandApplyAtRules.js @@ -8,18 +8,30 @@ import escapeClassName from '../util/escapeClassName' /** @typedef {Map} ApplyCache */ function extractClasses(node) { - let classes = new Set() + /** @type {Map>} */ + 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. diff --git a/tests/apply.test.js b/tests/apply.test.js index 7f149617e53a..7033300f01a6 100644 --- a/tests/apply.test.js +++ b/tests/apply.test.js @@ -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`
` }], + 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`
` }], + 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`
` }], + 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 },