Skip to content

Commit

Permalink
improve extractCandidates
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
RobinMalfait committed Jan 6, 2022
1 parent 82f163d commit e848e1e
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 10 deletions.
36 changes: 26 additions & 10 deletions src/lib/setupContextUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
98 changes: 98 additions & 0 deletions tests/apply.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down

0 comments on commit e848e1e

Please sign in to comment.