Repository: tailwindlabs/tailwindcss

Problem statement:
applyComplexClasses compiler stuck with @apply inside media queries or variants
### Describe the problem:

With the new applyComplexClasses if you put `@apply` **the old way**: inside a `@screen` (media query) or a `&:hover` (variant), the complier gets stuck (using postcss loader in webpack).

### Link to a minimal reproduction:

This css makes the compiler gets stuck (apply content can be anything):

```
.selector {
  @screen md {
    @apply w-2/6;
  }
}
```

```
.selector {
  &:hover {
    @apply text-white;
  }
}
```

I know the I should use the new way to `@apply md:w-2/6` and `@apply hover:text-white`, but for backward compatibility could be good fixing this bug, or at least give an error in the compiler, right now it just gets stuck compiling.


Hints:
This should be fixed mainly because it bugs also when you apply custom selectors where you don't have variants in the `@apply`:

```
.selector {
  @screen lg {
    @apply overflow-sub;
  }
}
```

Where `@apply lg:overflow-sub` is not possible when you don't want to set up apply variants for this selector.
This is a tough one to fix but I'm sure we can sort it out. In the mean time I would move your nesting plugin _before_ Tailwind so that there is no nested CSS left once the CSS is processed by Tailwind.
@adamwathan I moved **postcss-nested** _before_ Tailwind plugin, but still my build stuck
```
plugins: {
      'postcss-import': {},
      'postcss-nested': {},
      tailwindcss: {},
      'postcss-hexrgba': {},
      'postcss-custom-properties': {
        importFrom: [{customProperties}]
      },
      autoprefixer: {}
    }
```
@AndrewBogdanovTSS Can you share a repository that reproduces the issue? Way too hard to troubleshoot without unfortunately.
Edit: removed because not relevant to this issue
Please for the love of god someone just make a GitHub repository so I don't have to make one from scratch and I can probably fix it in 5 minutes between dad duty, but if I have to do it all myself it'll be days before I have a chance to play with it.
@adamwathan Created one for you: https://github.com/thecrypticace/at-apply-stuck
I'll note that it seems to get stuck with or without `postcss-nested`.
The bubble option in `postcss-nested` seems to just do nothing currently which is a problem. Need to configure it to bubble `screen` rules like this:

```js
module.exports = {
  plugins: {
    'postcss-nested': {
      bubble: ['screen'],
    },
    tailwindcss: {},
  },
}
```

But it's not bubbling them :/
Actually nevermind the problem is the reproduction is setup incorrectly, it has a postcss.config.js file but it's using `tailwind` to build the CSS instead of PostCSS, so the config isn't being read.

I've set it up correctly and verified it's a bug in postcss-nested that prevents this from working currently unfortunately.

One fix I can think of is to write a separate PostCSS plugin that converts screen rules to media queries and switch to `postcss-nesting`, because nesting media queries with postcss-nested while using `apply` seems broken right now, even without Tailwind in the build process at all. It just doesn't produce the expected CSS to feed into Tailwind.

Here's my repo which shows the current bug:

https://github.com/adamwathan/at-apply-stuck


This is the bug we'd need to fix to have this working sensibly:

https://github.com/postcss/postcss-nested/issues/81

Best solution currently is just to not nest screen rules if you need to use apply, write them at the root level like regular CSS instead for now.
Ah whoops, sorry about that.
Since it still gets stuck w/o postcss-nested should tailwind still detect that and handle it gracefully (at least not seeming get stuck in a loop)?
It should probably just actually work so if we are going to make any changes to Tailwind making it work is the change I'd want to make. Just trying to offer some workaround in the mean time while I'm at some adventure farm with my 2 year old.
> It should probably just actually work so if we are going to make any changes to Tailwind making it work is the change I'd want to make. Just trying to offer some workaround in the mean time while I'm at some adventure farm with my 2 year old.

You're awesome, next time I'll make a github with the problem sorry for my slack
Making this _work_ work is going to be a bit challenging but necessary anyways since there is a real CSS nesting spec and it's gonna probably exist for real one day.

Just a note for myself, I think the correct algorithm for applying styles in these situations is to generate the "applyable" selector (with the __TAILWIND-APPLY-PLACEHOLDER__ attribute selector), and split it on the placeholder, then stick the first half on the beginning of the _first_ selector in the tree, and the last half at the end of the _last_ selector in the tree.

Example:

```css
.foo {
  .bar {
    .baz {
      @apply group-hover:opacity-50 hover:font-bold;
      // Applyable rules here are:
      //  .group\:hover [__TAILWIND-APPLY-PLACEHOLDER__]
      //  [__TAILWIND-APPLY-PLACEHOLDER__]:hover
    }
  }
}
```

Output should be:


```css
.group\:hover .foo {
  .bar {
    .baz {
      opacity: .5;
    }
  }
}


.foo {
  .bar {
    .baz:hover {
      font-weight: 700;
    }
  }
}
```

Haven't thought through this bit in too much detail yet but another note for myself, think through what happens when people use `&` at the end of a nested selector because that could be dark.

Patch:
diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js
--- 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()
       }
     })
 


Test patch:
diff --git a/__tests__/applyComplexClasses.test.js b/__tests__/applyComplexClasses.test.js
--- 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)
+  })
+})