Skip to content

Commit

Permalink
Complex apply stuck (#2271)
Browse files Browse the repository at this point in the history
* dry-up duplication

* fix: apply inside a nested structure
  • Loading branch information
RobinMalfait authored Sep 7, 2020
1 parent 0cf76cd commit 3a25c4b
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 69 deletions.
98 changes: 98 additions & 0 deletions __tests__/applyComplexClasses.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -996,3 +996,101 @@ test('you can apply classes to a rule with multiple selectors', () => {
expect(result.warnings().length).toBe(0)
})
})

test('you can apply classes in a nested rule', () => {
const input = `
.selector {
&:hover {
@apply text-white;
}
}
`

const expected = `
.selector {
&:hover {
--text-opacity: 1;
color: #fff;
color: rgba(255, 255, 255, var(--text-opacity));
}
}
`

return run(input).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})

test('you can apply classes in a nested @atrule', () => {
const input = `
.selector {
@media (min-width: 200px) {
@apply overflow-hidden;
}
}
`

const expected = `
.selector {
@media (min-width: 200px) {
overflow: hidden;
}
}
`

return run(input).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})

test('you can apply classes in a custom nested @atrule', () => {
const input = `
.selector {
@screen md {
@apply w-2/6;
}
}
`

const expected = `
.selector {
@media (min-width: 768px) {
width: 33.333333%;
}
}
`

return run(input).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})

test('you can deeply apply classes in a custom nested @atrule', () => {
const input = `
.selector {
.subselector {
@screen md {
@apply w-2/6;
}
}
}
`

const expected = `
.selector {
.subselector {
@media (min-width: 768px) {
width: 33.333333%
}
}
}
`

return run(input).then(result => {
expect(result.css).toMatchCss(expected)
expect(result.warnings().length).toBe(0)
})
})
128 changes: 59 additions & 69 deletions src/flagged/applyComplexClasses.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ function buildUtilityMap(css, lookupTree) {
let index = 0
const utilityMap = {}

lookupTree.walkRules(rule => {
function handle(rule) {
const utilityNames = extractUtilityNames(rule.selector)

utilityNames.forEach((utilityName, i) => {
Expand All @@ -113,27 +113,10 @@ function buildUtilityMap(css, lookupTree) {
})
index++
})
})

css.walkRules(rule => {
const utilityNames = extractUtilityNames(rule.selector)

utilityNames.forEach((utilityName, i) => {
if (utilityMap[utilityName] === undefined) {
utilityMap[utilityName] = []
}
}

utilityMap[utilityName].push({
index,
utilityName,
classPosition: i,
get rule() {
return cloneRuleWithParent(rule)
},
})
index++
})
})
lookupTree.walkRules(handle)
css.walkRules(handle)

return utilityMap
}
Expand Down Expand Up @@ -203,68 +186,75 @@ function makeExtractUtilityRules(css, lookupTree, config) {
}
}

function findParent(rule, predicate) {
let parent = rule.parent
while (parent) {
if (predicate(parent)) {
return parent
}

parent = parent.parent
}

throw new Error('No parent could be found')
}

function processApplyAtRules(css, lookupTree, config) {
const extractUtilityRules = makeExtractUtilityRules(css, lookupTree, config)

do {
css.walkRules(rule => {
const applyRules = []
css.walkAtRules('apply', applyRule => {
const parent = applyRule.parent // Direct parent
const nearestParentRule = findParent(applyRule, r => r.type === 'rule')
const currentUtilityNames = extractUtilityNames(nearestParentRule.selector)

const [
importantEntries,
applyUtilityNames,
important = importantEntries.length > 0,
] = _.partition(applyRule.params.split(/[\s\t\n]+/g), n => n === '!important')

if (_.intersection(applyUtilityNames, currentUtilityNames).length > 0) {
const currentUtilityName = _.intersection(applyUtilityNames, currentUtilityNames)[0]
throw parent.error(
`You cannot \`@apply\` the \`${currentUtilityName}\` utility here because it creates a circular dependency.`
)
}

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

applyRules.forEach(applyRule => {
const [
importantEntries,
applyUtilityNames,
important = importantEntries.length > 0,
] = _.partition(applyRule.params.split(/[\s\t\n]+/g), n => n === '!important')
// Sort applys to match CSS source order
const applys = extractUtilityRules(applyUtilityNames, applyRule)

const currentUtilityNames = extractUtilityNames(rule.selector)
// Get new rules with the utility portion of the selector replaced with the new selector
const rulesToInsert = []

if (_.intersection(applyUtilityNames, currentUtilityNames).length > 0) {
const currentUtilityName = _.intersection(applyUtilityNames, currentUtilityNames)[0]
throw rule.error(
`You cannot \`@apply\` the \`${currentUtilityName}\` utility here because it creates a circular dependency.`
)
}
applys.forEach(
nearestParentRule === parent
? util => rulesToInsert.push(generateRulesFromApply(util, parent.selectors))
: util => util.rule.nodes.forEach(n => afterRule.append(n.clone()))
)

// Extract any post-apply declarations and re-insert them after apply rules
const afterRule = rule.clone({ raws: {} })
afterRule.nodes = afterRule.nodes.slice(rule.index(applyRule) + 1)
rule.nodes = rule.nodes.slice(0, rule.index(applyRule) + 1)

// Sort applys to match CSS source order
const applys = extractUtilityRules(applyUtilityNames, applyRule)

// Get new rules with the utility portion of the selector replaced with the new selector
const rulesToInsert = [
...applys.map(applyUtility => {
return generateRulesFromApply(applyUtility, rule.selectors)
}),
afterRule,
]

const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), root =>
root.walkDecls(d => {
d.important = important
})
)
rulesToInsert.push(afterRule)

const mergedRules = mergeAdjacentRules(rule, nodes)
const { nodes } = _.tap(postcss.root({ nodes: rulesToInsert }), root =>
root.walkDecls(d => {
d.important = important
})
)

applyRule.remove()
rule.after(mergedRules)
})
const mergedRules = mergeAdjacentRules(nearestParentRule, nodes)

applyRule.remove()
parent.after(mergedRules)

// If the base rule has nothing in it (all applys were pseudo or responsive variants),
// remove the rule fuggit.
if (rule.nodes.length === 0) {
rule.remove()
if (parent.nodes.length === 0) {
parent.remove()
}
})

Expand Down

0 comments on commit 3a25c4b

Please sign in to comment.