Skip to content

Commit ac61c31

Browse files
committed
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 usefull 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 clases. 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!
1 parent 82f163d commit ac61c31

File tree

2 files changed

+124
-10
lines changed

2 files changed

+124
-10
lines changed

src/lib/setupContextUtils.js

+26-10
Original file line numberDiff line numberDiff line change
@@ -89,39 +89,55 @@ function getClasses(selector) {
8989
return parser.transformSync(selector)
9090
}
9191

92-
function extractCandidates(node) {
92+
function extractCandidates(node, state = { containsNonOnDemandable: false }, depth = 0) {
9393
let classes = []
9494

95+
// Handle normal rules
9596
if (node.type === 'rule') {
9697
for (let selector of node.selectors) {
9798
let classCandidates = getClasses(selector)
9899
// At least one of the selectors contains non-"on-demandable" candidates.
99-
if (classCandidates.length === 0) return []
100+
if (classCandidates.length === 0) {
101+
state.containsNonOnDemandable = true
102+
}
100103

101-
classes = [...classes, ...classCandidates]
104+
for (let classCandidate of classCandidates) {
105+
classes.push(classCandidate)
106+
}
102107
}
103-
return classes
104108
}
105109

106-
if (node.type === 'atrule') {
110+
// Handle at-rules (which contains nested rules)
111+
else if (node.type === 'atrule') {
107112
node.walkRules((rule) => {
108-
classes = [...classes, ...rule.selectors.flatMap((selector) => getClasses(selector))]
113+
for (let classCandidate of rule.selectors.flatMap((selector) =>
114+
getClasses(selector, state, depth + 1)
115+
)) {
116+
classes.push(classCandidate)
117+
}
109118
})
110119
}
111120

121+
if (depth === 0) {
122+
return [state.containsNonOnDemandable || classes.length === 0, classes]
123+
}
124+
112125
return classes
113126
}
114127

115128
function withIdentifiers(styles) {
116129
return parseStyles(styles).flatMap((node) => {
117130
let nodeMap = new Map()
118-
let candidates = extractCandidates(node)
131+
let [containsNonOnDemandableSelectors, candidates] = extractCandidates(node)
119132

120-
// If this isn't "on-demandable", assign it a universal candidate.
121-
if (candidates.length === 0) {
122-
return [['*', node]]
133+
// If this isn't "on-demandable", assign it a universal candidate to always include it.
134+
if (containsNonOnDemandableSelectors) {
135+
candidates.unshift('*')
123136
}
124137

138+
// However, it could be that it also contains "on-demandable" candidates.
139+
// E.g.: `span, .foo {}`, in that case it should still be possible to use
140+
// `@apply foo` for example.
125141
return candidates.map((c) => {
126142
if (!nodeMap.has(node)) {
127143
nodeMap.set(node, node)

tests/apply.test.js

+98
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,104 @@ it('should be possible to apply user css without tailwind directives', () => {
812812
})
813813
})
814814

815+
it('should be possible to apply a class from another rule with multiple selectors (2 classes)', () => {
816+
let config = {
817+
content: [{ raw: html`<div class="c"></div>` }],
818+
plugins: [],
819+
}
820+
821+
let input = css`
822+
@tailwind utilities;
823+
@layer utilities {
824+
.a,
825+
.b {
826+
@apply underline;
827+
}
828+
829+
.c {
830+
@apply b;
831+
}
832+
}
833+
`
834+
835+
return run(input, config).then((result) => {
836+
return expect(result.css).toMatchFormattedCss(css`
837+
.c {
838+
text-decoration-line: underline;
839+
}
840+
`)
841+
})
842+
})
843+
844+
it('should be possible to apply a class from another rule with multiple selectors (1 class, 1 tag)', () => {
845+
let config = {
846+
content: [{ raw: html`<div class="c"></div>` }],
847+
plugins: [],
848+
}
849+
850+
let input = css`
851+
@tailwind utilities;
852+
853+
@layer utilities {
854+
span,
855+
.b {
856+
@apply underline;
857+
}
858+
859+
.c {
860+
@apply b;
861+
}
862+
}
863+
`
864+
865+
return run(input, config).then((result) => {
866+
return expect(result.css).toMatchFormattedCss(css`
867+
span,
868+
.b {
869+
text-decoration-line: underline;
870+
}
871+
872+
.c {
873+
text-decoration-line: underline;
874+
}
875+
`)
876+
})
877+
})
878+
879+
it('should be possible to apply a class from another rule with multiple selectors (1 class, 1 id)', () => {
880+
let config = {
881+
content: [{ raw: html`<div class="c"></div>` }],
882+
plugins: [],
883+
}
884+
885+
let input = css`
886+
@tailwind utilities;
887+
@layer utilities {
888+
#a,
889+
.b {
890+
@apply underline;
891+
}
892+
893+
.c {
894+
@apply b;
895+
}
896+
}
897+
`
898+
899+
return run(input, config).then((result) => {
900+
return expect(result.css).toMatchFormattedCss(css`
901+
#a,
902+
.b {
903+
text-decoration-line: underline;
904+
}
905+
906+
.c {
907+
text-decoration-line: underline;
908+
}
909+
`)
910+
})
911+
})
912+
815913
/*
816914
it('apply can emit defaults in isolated environments without @tailwind directives', () => {
817915
let config = {

0 commit comments

Comments
 (0)