Skip to content

Commit 3a25c4b

Browse files
authored
Complex apply stuck (#2271)
* dry-up duplication * fix: apply inside a nested structure
1 parent 0cf76cd commit 3a25c4b

File tree

2 files changed

+157
-69
lines changed

2 files changed

+157
-69
lines changed

__tests__/applyComplexClasses.test.js

+98
Original file line numberDiff line numberDiff line change
@@ -996,3 +996,101 @@ test('you can apply classes to a rule with multiple selectors', () => {
996996
expect(result.warnings().length).toBe(0)
997997
})
998998
})
999+
1000+
test('you can apply classes in a nested rule', () => {
1001+
const input = `
1002+
.selector {
1003+
&:hover {
1004+
@apply text-white;
1005+
}
1006+
}
1007+
`
1008+
1009+
const expected = `
1010+
.selector {
1011+
&:hover {
1012+
--text-opacity: 1;
1013+
color: #fff;
1014+
color: rgba(255, 255, 255, var(--text-opacity));
1015+
}
1016+
}
1017+
`
1018+
1019+
return run(input).then(result => {
1020+
expect(result.css).toMatchCss(expected)
1021+
expect(result.warnings().length).toBe(0)
1022+
})
1023+
})
1024+
1025+
test('you can apply classes in a nested @atrule', () => {
1026+
const input = `
1027+
.selector {
1028+
@media (min-width: 200px) {
1029+
@apply overflow-hidden;
1030+
}
1031+
}
1032+
`
1033+
1034+
const expected = `
1035+
.selector {
1036+
@media (min-width: 200px) {
1037+
overflow: hidden;
1038+
}
1039+
}
1040+
`
1041+
1042+
return run(input).then(result => {
1043+
expect(result.css).toMatchCss(expected)
1044+
expect(result.warnings().length).toBe(0)
1045+
})
1046+
})
1047+
1048+
test('you can apply classes in a custom nested @atrule', () => {
1049+
const input = `
1050+
.selector {
1051+
@screen md {
1052+
@apply w-2/6;
1053+
}
1054+
}
1055+
`
1056+
1057+
const expected = `
1058+
.selector {
1059+
@media (min-width: 768px) {
1060+
width: 33.333333%;
1061+
}
1062+
}
1063+
`
1064+
1065+
return run(input).then(result => {
1066+
expect(result.css).toMatchCss(expected)
1067+
expect(result.warnings().length).toBe(0)
1068+
})
1069+
})
1070+
1071+
test('you can deeply apply classes in a custom nested @atrule', () => {
1072+
const input = `
1073+
.selector {
1074+
.subselector {
1075+
@screen md {
1076+
@apply w-2/6;
1077+
}
1078+
}
1079+
}
1080+
`
1081+
1082+
const expected = `
1083+
.selector {
1084+
.subselector {
1085+
@media (min-width: 768px) {
1086+
width: 33.333333%
1087+
}
1088+
}
1089+
}
1090+
`
1091+
1092+
return run(input).then(result => {
1093+
expect(result.css).toMatchCss(expected)
1094+
expect(result.warnings().length).toBe(0)
1095+
})
1096+
})

src/flagged/applyComplexClasses.js

+59-69
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ function buildUtilityMap(css, lookupTree) {
9595
let index = 0
9696
const utilityMap = {}
9797

98-
lookupTree.walkRules(rule => {
98+
function handle(rule) {
9999
const utilityNames = extractUtilityNames(rule.selector)
100100

101101
utilityNames.forEach((utilityName, i) => {
@@ -113,27 +113,10 @@ function buildUtilityMap(css, lookupTree) {
113113
})
114114
index++
115115
})
116-
})
117-
118-
css.walkRules(rule => {
119-
const utilityNames = extractUtilityNames(rule.selector)
120-
121-
utilityNames.forEach((utilityName, i) => {
122-
if (utilityMap[utilityName] === undefined) {
123-
utilityMap[utilityName] = []
124-
}
116+
}
125117

126-
utilityMap[utilityName].push({
127-
index,
128-
utilityName,
129-
classPosition: i,
130-
get rule() {
131-
return cloneRuleWithParent(rule)
132-
},
133-
})
134-
index++
135-
})
136-
})
118+
lookupTree.walkRules(handle)
119+
css.walkRules(handle)
137120

138121
return utilityMap
139122
}
@@ -203,68 +186,75 @@ function makeExtractUtilityRules(css, lookupTree, config) {
203186
}
204187
}
205188

189+
function findParent(rule, predicate) {
190+
let parent = rule.parent
191+
while (parent) {
192+
if (predicate(parent)) {
193+
return parent
194+
}
195+
196+
parent = parent.parent
197+
}
198+
199+
throw new Error('No parent could be found')
200+
}
201+
206202
function processApplyAtRules(css, lookupTree, config) {
207203
const extractUtilityRules = makeExtractUtilityRules(css, lookupTree, config)
208204

209205
do {
210-
css.walkRules(rule => {
211-
const applyRules = []
206+
css.walkAtRules('apply', applyRule => {
207+
const parent = applyRule.parent // Direct parent
208+
const nearestParentRule = findParent(applyRule, r => r.type === 'rule')
209+
const currentUtilityNames = extractUtilityNames(nearestParentRule.selector)
210+
211+
const [
212+
importantEntries,
213+
applyUtilityNames,
214+
important = importantEntries.length > 0,
215+
] = _.partition(applyRule.params.split(/[\s\t\n]+/g), n => n === '!important')
216+
217+
if (_.intersection(applyUtilityNames, currentUtilityNames).length > 0) {
218+
const currentUtilityName = _.intersection(applyUtilityNames, currentUtilityNames)[0]
219+
throw parent.error(
220+
`You cannot \`@apply\` the \`${currentUtilityName}\` utility here because it creates a circular dependency.`
221+
)
222+
}
212223

213-
// Only walk direct children to avoid issues with nesting plugins
214-
rule.each(child => {
215-
if (child.type === 'atrule' && child.name === 'apply') {
216-
applyRules.unshift(child)
217-
}
218-
})
224+
// Extract any post-apply declarations and re-insert them after apply rules
225+
const afterRule = parent.clone({ raws: {} })
226+
afterRule.nodes = afterRule.nodes.slice(parent.index(applyRule) + 1)
227+
parent.nodes = parent.nodes.slice(0, parent.index(applyRule) + 1)
219228

220-
applyRules.forEach(applyRule => {
221-
const [
222-
importantEntries,
223-
applyUtilityNames,
224-
important = importantEntries.length > 0,
225-
] = _.partition(applyRule.params.split(/[\s\t\n]+/g), n => n === '!important')
229+
// Sort applys to match CSS source order
230+
const applys = extractUtilityRules(applyUtilityNames, applyRule)
226231

227-
const currentUtilityNames = extractUtilityNames(rule.selector)
232+
// Get new rules with the utility portion of the selector replaced with the new selector
233+
const rulesToInsert = []
228234

229-
if (_.intersection(applyUtilityNames, currentUtilityNames).length > 0) {
230-
const currentUtilityName = _.intersection(applyUtilityNames, currentUtilityNames)[0]
231-
throw rule.error(
232-
`You cannot \`@apply\` the \`${currentUtilityName}\` utility here because it creates a circular dependency.`
233-
)
234-
}
235+
applys.forEach(
236+
nearestParentRule === parent
237+
? util => rulesToInsert.push(generateRulesFromApply(util, parent.selectors))
238+
: util => util.rule.nodes.forEach(n => afterRule.append(n.clone()))
239+
)
235240

236-
// Extract any post-apply declarations and re-insert them after apply rules
237-
const afterRule = rule.clone({ raws: {} })
238-
afterRule.nodes = afterRule.nodes.slice(rule.index(applyRule) + 1)
239-
rule.nodes = rule.nodes.slice(0, rule.index(applyRule) + 1)
240-
241-
// Sort applys to match CSS source order
242-
const applys = extractUtilityRules(applyUtilityNames, applyRule)
243-
244-
// Get new rules with the utility portion of the selector replaced with the new selector
245-
const rulesToInsert = [
246-
...applys.map(applyUtility => {
247-
return generateRulesFromApply(applyUtility, rule.selectors)
248-
}),
249-
afterRule,
250-
]
251-
252-
const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), root =>
253-
root.walkDecls(d => {
254-
d.important = important
255-
})
256-
)
241+
rulesToInsert.push(afterRule)
257242

258-
const mergedRules = mergeAdjacentRules(rule, nodes)
243+
const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), root =>
244+
root.walkDecls(d => {
245+
d.important = important
246+
})
247+
)
259248

260-
applyRule.remove()
261-
rule.after(mergedRules)
262-
})
249+
const mergedRules = mergeAdjacentRules(nearestParentRule, nodes)
250+
251+
applyRule.remove()
252+
parent.after(mergedRules)
263253

264254
// If the base rule has nothing in it (all applys were pseudo or responsive variants),
265255
// remove the rule fuggit.
266-
if (rule.nodes.length === 0) {
267-
rule.remove()
256+
if (parent.nodes.length === 0) {
257+
parent.remove()
268258
}
269259
})
270260

0 commit comments

Comments
 (0)