From 3a25c4bee9082205b2332df4e802c9db45cbc142 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 7 Sep 2020 02:36:09 +0200 Subject: [PATCH] Complex apply stuck (#2271) * dry-up duplication * fix: apply inside a nested structure --- __tests__/applyComplexClasses.test.js | 98 ++++++++++++++++++++ src/flagged/applyComplexClasses.js | 128 ++++++++++++-------------- 2 files changed, 157 insertions(+), 69 deletions(-) diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js index 9112d2c6d57c..2301fb400471 100644 --- a/__tests__/applyComplexClasses.test.js +++ b/__tests__/applyComplexClasses.test.js @@ -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) + }) +}) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 7e8d95bff9dd..23612af53de4 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -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) => { @@ -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 } @@ -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() } })