Skip to content
This repository was archived by the owner on Apr 6, 2021. It is now read-only.

Commit 4a55876

Browse files
authored
Respect apply sibling order (#155)
1 parent a718c06 commit 4a55876

File tree

4 files changed

+188
-108
lines changed

4 files changed

+188
-108
lines changed

src/lib/expandApplyAtRules.js

Lines changed: 152 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -40,135 +40,182 @@ function extractApplyCandidates(params) {
4040
return [candidates, false]
4141
}
4242

43-
function expandApplyAtRules(context) {
44-
return function processApply(root) {
45-
let applyCandidates = new Set()
43+
function partitionApplyParents(root) {
44+
let applyParents = new Set()
45+
46+
root.walkAtRules('apply', (rule) => {
47+
applyParents.add(rule.parent)
48+
})
49+
50+
for (let rule of applyParents) {
51+
let nodeGroups = []
52+
let lastGroup = []
53+
54+
for (let node of rule.nodes) {
55+
if (node.type === 'atrule' && node.name === 'apply') {
56+
if (lastGroup.length > 0) {
57+
nodeGroups.push(lastGroup)
58+
lastGroup = []
59+
}
60+
nodeGroups.push([node])
61+
} else {
62+
lastGroup.push(node)
63+
}
64+
}
4665

47-
// Collect all @apply rules and candidates
48-
let applies = []
49-
root.walkAtRules('apply', (rule) => {
50-
let [candidates, important] = extractApplyCandidates(rule.params)
66+
if (lastGroup.length > 0) {
67+
nodeGroups.push(lastGroup)
68+
}
5169

52-
for (let util of candidates) {
53-
applyCandidates.add(util)
54-
}
55-
applies.push(rule)
56-
})
57-
58-
// Start the @apply process if we have rules with @apply in them
59-
if (applies.length > 0) {
60-
// Fill up some caches!
61-
let applyClassCache = buildApplyCache(applyCandidates, context)
62-
63-
/**
64-
* When we have an apply like this:
65-
*
66-
* .abc {
67-
* @apply hover:font-bold;
68-
* }
69-
*
70-
* What we essentially will do is resolve to this:
71-
*
72-
* .abc {
73-
* @apply .hover\:font-bold:hover {
74-
* font-weight: 500;
75-
* }
76-
* }
77-
*
78-
* Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
79-
* What happens in this function is that we prepend a `.` and escape the candidate.
80-
* This will result in `.hover\:font-bold`
81-
* Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
82-
*/
83-
// TODO: Should we use postcss-selector-parser for this instead?
84-
function replaceSelector(selector, utilitySelectors, candidate) {
85-
let needle = `.${escapeClassName(candidate)}`
86-
let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g)
87-
88-
return selector
89-
.split(/\s*,\s*/g)
90-
.map((s) => {
91-
let replaced = []
92-
93-
for (let utilitySelector of utilitySelectorsList) {
94-
let replacedSelector = utilitySelector.replace(needle, s)
95-
if (replacedSelector === utilitySelector) {
96-
continue
97-
}
98-
replaced.push(replacedSelector)
99-
}
100-
return replaced.join(', ')
101-
})
102-
.join(', ')
103-
}
70+
if (nodeGroups.length === 1) {
71+
continue
72+
}
10473

105-
/** @type {Map<import('postcss').Node, [string, boolean, import('postcss').Node[]][]>} */
106-
let perParentApplies = new Map()
74+
for (let group of [...nodeGroups].reverse()) {
75+
let newParent = rule.clone({ nodes: [] })
76+
newParent.append(group)
77+
rule.after(newParent)
78+
}
10779

108-
// Collect all apply candidates and their rules
109-
for (let apply of applies) {
110-
let candidates = perParentApplies.get(apply.parent) || []
80+
rule.remove()
81+
}
82+
}
11183

112-
perParentApplies.set(apply.parent, candidates)
84+
function processApply(root, context) {
85+
let applyCandidates = new Set()
11386

114-
let [applyCandidates, important] = extractApplyCandidates(apply.params)
87+
// Collect all @apply rules and candidates
88+
let applies = []
89+
root.walkAtRules('apply', (rule) => {
90+
let [candidates] = extractApplyCandidates(rule.params)
11591

116-
for (let applyCandidate of applyCandidates) {
117-
if (!applyClassCache.has(applyCandidate)) {
118-
throw apply.error(
119-
`The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
120-
)
92+
for (let util of candidates) {
93+
applyCandidates.add(util)
94+
}
95+
applies.push(rule)
96+
})
97+
98+
// Start the @apply process if we have rules with @apply in them
99+
if (applies.length > 0) {
100+
// Fill up some caches!
101+
let applyClassCache = buildApplyCache(applyCandidates, context)
102+
103+
/**
104+
* When we have an apply like this:
105+
*
106+
* .abc {
107+
* @apply hover:font-bold;
108+
* }
109+
*
110+
* What we essentially will do is resolve to this:
111+
*
112+
* .abc {
113+
* @apply .hover\:font-bold:hover {
114+
* font-weight: 500;
115+
* }
116+
* }
117+
*
118+
* Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
119+
* What happens in this function is that we prepend a `.` and escape the candidate.
120+
* This will result in `.hover\:font-bold`
121+
* Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
122+
*/
123+
// TODO: Should we use postcss-selector-parser for this instead?
124+
function replaceSelector(selector, utilitySelectors, candidate) {
125+
let needle = `.${escapeClassName(candidate)}`
126+
let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g)
127+
128+
return selector
129+
.split(/\s*,\s*/g)
130+
.map((s) => {
131+
let replaced = []
132+
133+
for (let utilitySelector of utilitySelectorsList) {
134+
let replacedSelector = utilitySelector.replace(needle, s)
135+
if (replacedSelector === utilitySelector) {
136+
continue
137+
}
138+
replaced.push(replacedSelector)
121139
}
140+
return replaced.join(', ')
141+
})
142+
.join(', ')
143+
}
144+
145+
/** @type {Map<import('postcss').Node, [string, boolean, import('postcss').Node[]][]>} */
146+
let perParentApplies = new Map()
147+
148+
// Collect all apply candidates and their rules
149+
for (let apply of applies) {
150+
let candidates = perParentApplies.get(apply.parent) || []
122151

123-
let rules = applyClassCache.get(applyCandidate)
152+
perParentApplies.set(apply.parent, candidates)
124153

125-
candidates.push([applyCandidate, important, rules])
154+
let [applyCandidates, important] = extractApplyCandidates(apply.params)
155+
156+
for (let applyCandidate of applyCandidates) {
157+
if (!applyClassCache.has(applyCandidate)) {
158+
throw apply.error(
159+
`The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
160+
)
126161
}
162+
163+
let rules = applyClassCache.get(applyCandidate)
164+
165+
candidates.push([applyCandidate, important, rules])
127166
}
167+
}
128168

129-
for (const [parent, candidates] of perParentApplies) {
130-
let siblings = []
169+
for (const [parent, candidates] of perParentApplies) {
170+
let siblings = []
131171

132-
for (let [applyCandidate, important, rules] of candidates) {
133-
for (let [meta, node] of rules) {
134-
let root = postcss.root({ nodes: [node.clone()] })
135-
let canRewriteSelector =
136-
node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
172+
for (let [applyCandidate, important, rules] of candidates) {
173+
for (let [meta, node] of rules) {
174+
let root = postcss.root({ nodes: [node.clone()] })
175+
let canRewriteSelector =
176+
node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
137177

138-
if (canRewriteSelector) {
139-
root.walkRules((rule) => {
140-
rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate)
178+
if (canRewriteSelector) {
179+
root.walkRules((rule) => {
180+
rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate)
141181

142-
rule.walkDecls((d) => {
143-
d.important = important
144-
})
182+
rule.walkDecls((d) => {
183+
d.important = important
145184
})
146-
}
147-
148-
siblings.push([meta, root.nodes[0]])
185+
})
149186
}
187+
188+
siblings.push([meta, root.nodes[0]])
150189
}
190+
}
151191

152-
// Inject the rules, sorted, correctly
153-
const nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
192+
// Inject the rules, sorted, correctly
193+
let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
154194

155-
// `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
156-
parent.after(nodes)
157-
}
195+
// console.log(parent)
196+
// `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
197+
parent.after(nodes)
198+
}
158199

159-
for (let apply of applies) {
160-
// If there are left-over declarations, just remove the @apply
161-
if (apply.parent.nodes.length > 1) {
162-
apply.remove()
163-
} else {
164-
// The node is empty, drop the full node
165-
apply.parent.remove()
166-
}
200+
for (let apply of applies) {
201+
// If there are left-over declarations, just remove the @apply
202+
if (apply.parent.nodes.length > 1) {
203+
apply.remove()
204+
} else {
205+
// The node is empty, drop the full node
206+
apply.parent.remove()
167207
}
168-
169-
// Do it again, in case we have other `@apply` rules
170-
processApply(root)
171208
}
209+
210+
// Do it again, in case we have other `@apply` rules
211+
processApply(root, context)
212+
}
213+
}
214+
215+
function expandApplyAtRules(context) {
216+
return (root) => {
217+
partitionApplyParents(root)
218+
processApply(root, context)
172219
}
173220
}
174221

tests/10-apply.test.css

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,32 @@
265265
color: green;
266266
font-weight: 700;
267267
}
268+
.add-sibling-properties {
269+
padding: 2rem;
270+
padding-left: 1rem;
271+
padding-right: 1rem;
272+
}
273+
.add-sibling-properties:hover {
274+
padding-left: 0.5rem;
275+
padding-right: 0.5rem;
276+
}
277+
@media (min-width: 1024px) {
278+
.add-sibling-properties {
279+
padding-left: 2.5rem;
280+
padding-right: 2.5rem;
281+
}
282+
}
283+
@media (min-width: 1280px) {
284+
.add-sibling-properties:focus {
285+
padding-left: 0.25rem;
286+
padding-right: 0.25rem;
287+
}
288+
}
289+
.add-sibling-properties {
290+
padding-top: 3px;
291+
color: green;
292+
font-weight: 700;
293+
}
268294
h1 {
269295
font-size: 1.5rem;
270296
line-height: 2rem;
@@ -285,13 +311,13 @@ h2 {
285311
font-size: 1.5rem;
286312
line-height: 2rem;
287313
}
288-
@media (min-width: 640px) {
314+
@media (min-width: 1024px) {
289315
h2 {
290316
font-size: 1.5rem;
291317
line-height: 2rem;
292318
}
293319
}
294-
@media (min-width: 1024px) {
320+
@media (min-width: 640px) {
295321
h2 {
296322
font-size: 1.5rem;
297323
line-height: 2rem;

tests/10-apply.test.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
<div class="recursive-apply-b"></div>
3232
<div class="recursive-apply-c"></div>
3333
<div class="use-with-other-properties-base use-with-other-properties-component"></div>
34+
<div class="add-sibling-properties"></div>
3435
<div class="a b"></div>
3536
<div class="foo"></div>
3637
<div class="bar"></div>

tests/10-apply.test.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ test('@apply', () => {
7878
@apply use-dependant-only-a font-normal;
7979
}
8080
.btn {
81-
@apply font-bold py-2 px-4 rounded;
81+
@apply font-bold py-2 px-4 rounded;
8282
}
8383
.btn-blue {
8484
@apply btn bg-blue-500 hover:bg-blue-700 text-white;
@@ -99,6 +99,12 @@ test('@apply', () => {
9999
.use-with-other-properties-component {
100100
@apply use-with-other-properties-base;
101101
}
102+
.add-sibling-properties {
103+
padding: 2rem;
104+
@apply px-4 hover:px-2 lg:px-10 xl:focus:px-1;
105+
padding-top: 3px;
106+
@apply use-with-other-properties-base;
107+
}
102108
103109
h1 {
104110
@apply text-2xl lg:text-2xl sm:text-3xl;

0 commit comments

Comments
 (0)