Ensure we can apply classes defined with non-"on-demandable" selectors (#6922)

* improve extractCandidates

When we have a css rule that is defined as `.foo, .bar {}`, then we will
crawl each selector and link it to the same node. This is useful because
now our Map looks something like this:

```js
Map(2) { 'foo' => Node {}, 'bar' => Node {} }
```

This allows us to later on `@apply foo` or `@apply bar` and we can do a
direct lookup for this "candidate".

When we have css defined as `span {}`, then we consider this
"non-ondemandable". This means that we will _always_ inject these rules
into the `*` section and call it a day.

However, it could happen that you have something like this: `span, .foo
{}` up until now this was totally fine. It contains a non-ondemandable
selector (`span`) and therefore we injected this into that `*` section.

However, the issue occurs if you now try to `@apply foo`. Since we had
an early return for this use case it didn't endup in our Map from above
and now you get an error like:

```
The `foo` class does not exist. If `foo` is a custom class, make sure it
is defined within a `@layer` directive."
```

So instead what we will do is keep track whether or not a css rule
contains any on-demandable classes. If this is the case then we still
generate it always by putting it in that `*` section. However, we will
still register all on-demandable classes in our Map (in this case `.foo`).

This allows us to `@apply foo` again!

* update changelog
This commit is contained in:
Robin Malfait 2022-01-06 13:27:14 +01:00 committed by GitHub
parent 82f163d425
commit 21fe083db0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 125 additions and 10 deletions

View File

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Allow use of falsy values in theme config ([#6917](https://github.com/tailwindlabs/tailwindcss/pull/6917))
- Ensure we can apply classes defined with non-"on-demandable" selectors ([#6922](https://github.com/tailwindlabs/tailwindcss/pull/6922))
## [3.0.11] - 2022-01-05

View File

@ -89,39 +89,55 @@ function getClasses(selector) {
return parser.transformSync(selector)
}
function extractCandidates(node) {
function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) {
let classes = []
// Handle normal rules
if (node.type === 'rule') {
for (let selector of node.selectors) {
let classCandidates = getClasses(selector)
// At least one of the selectors contains non-"on-demandable" candidates.
if (classCandidates.length === 0) return []
if (classCandidates.length === 0) {
state.containsNonOnDemandable = true
}
classes = [...classes, ...classCandidates]
for (let classCandidate of classCandidates) {
classes.push(classCandidate)
}
}
return classes
}
if (node.type === 'atrule') {
// Handle at-rules (which contains nested rules)
else if (node.type === 'atrule') {
node.walkRules((rule) => {
classes = [...classes, ...rule.selectors.flatMap((selector) => getClasses(selector))]
for (let classCandidate of rule.selectors.flatMap((selector) =>
getClasses(selector, state, depth + 1)
)) {
classes.push(classCandidate)
}
})
}
if (depth === 0) {
return [state.containsNonOnDemandable || classes.length === 0, classes]
}
return classes
}
function withIdentifiers(styles) {
return parseStyles(styles).flatMap((node) => {
let nodeMap = new Map()
let candidates = extractCandidates(node)
let [containsNonOnDemandableSelectors, candidates] = extractCandidates(node)
// If this isn't "on-demandable", assign it a universal candidate.
if (candidates.length === 0) {
return [['*', node]]
// If this isn't "on-demandable", assign it a universal candidate to always include it.
if (containsNonOnDemandableSelectors) {
candidates.unshift('*')
}
// However, it could be that it also contains "on-demandable" candidates.
// E.g.: `span, .foo {}`, in that case it should still be possible to use
// `@apply foo` for example.
return candidates.map((c) => {
if (!nodeMap.has(node)) {
nodeMap.set(node, node)

View File

@ -812,6 +812,104 @@ it('should be possible to apply user css without tailwind directives', () => {
})
})
it('should be possible to apply a class from another rule with multiple selectors (2 classes)', () => {
let config = {
content: [{ raw: html`<div class="c"></div>` }],
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
.a,
.b {
@apply underline;
}
.c {
@apply b;
}
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
.c {
text-decoration-line: underline;
}
`)
})
})
it('should be possible to apply a class from another rule with multiple selectors (1 class, 1 tag)', () => {
let config = {
content: [{ raw: html`<div class="c"></div>` }],
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
span,
.b {
@apply underline;
}
.c {
@apply b;
}
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
span,
.b {
text-decoration-line: underline;
}
.c {
text-decoration-line: underline;
}
`)
})
})
it('should be possible to apply a class from another rule with multiple selectors (1 class, 1 id)', () => {
let config = {
content: [{ raw: html`<div class="c"></div>` }],
plugins: [],
}
let input = css`
@tailwind utilities;
@layer utilities {
#a,
.b {
@apply underline;
}
.c {
@apply b;
}
}
`
return run(input, config).then((result) => {
return expect(result.css).toMatchFormattedCss(css`
#a,
.b {
text-decoration-line: underline;
}
.c {
text-decoration-line: underline;
}
`)
})
})
/*
it('apply can emit defaults in isolated environments without @tailwind directives', () => {
let config = {