Repository: tailwindlabs/tailwindcss (TypeScript)

Issue:
Utility classes can't be built with @apply and custom props
<!-- Please provide all of the information requested below. We're a small team and without all of this information it's not possible for us to help and your bug report will be closed. -->

**What version of Tailwind CSS are you using?**

v3.0.0

**What build tool (or framework if it abstracts the build tool) are you using?**

PostCSS

**What version of Node.js are you using?**

v14.13.0

**What browser are you using?**

Chrome

**What operating system are you using?**

Windows

**Reproduction URL**

https://play.tailwindcss.com/qm3e5FDSEr

**Describe your issue**

I've made utility classes that have both Tailwind tokens and custom css. But these classes could no longer be used with `@apply` after upgrading to v3.

This seems to be a [regression from v2->v3](https://play.tailwindcss.com/CwBZvIVuPx?file=css).

I can mitigate the issue for now by [duplicating some classes](https://play.tailwindcss.com/7FFOWiZ88S?file=css), but that's not a great solution.


Hints:
same for me =(
Hey! So we're going to dig into this a little deeper, but as a workaround you can wrap these classes in an `@layer utilities` directive to get this working:

```css
@layer utilities {
  .a {
    @apply b;
  }

  .b {
    @apply uppercase;
    color: red;
  }
}
```

Here's an updated Play illustrating this: https://play.tailwindcss.com/ksUrE9fW01?file=css
Hi, I have a lot of custom css classes that I reuse via `@apply my-css-class`. Recently I upgraded to v3 and  things stopped working. I'd like to ask if there is any workaround apart from putting those classes into an `@layer utilities` as above? Since I took for granted that I can reuse those classes with `@apply`, they are over all places and it's not easy to find them all.
@Tony2 with `3.0.7` you don't need to use `@layer`, but each css that uses `@apply` will need to include a reference to `@tailwind`. I have a PR that resolves this to `2.X.X` behaviour #6580
thank you @garymathews for the hint!

I rely heavily on twcss for my work (an in-house component framework). This bug made me somewhat worried if my bet on twcss is the correct decision. Till v2 it has appeared rock-solid and very well-maintained. I understand that v3 is still relatively new, however being able to use `@apply my-class` is IMO a vital feature to keep css maintainable.


Patch:
diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js
--- a/src/lib/expandApplyAtRules.js
+++ b/src/lib/expandApplyAtRules.js
@@ -72,47 +72,6 @@ function extractApplyCandidates(params) {
   return [candidates, false]
 }
 
-function partitionApplyParents(root) {
-  let applyParents = new Set()
-
-  root.walkAtRules('apply', (rule) => {
-    applyParents.add(rule.parent)
-  })
-
-  for (let rule of applyParents) {
-    let nodeGroups = []
-    let lastGroup = []
-
-    for (let node of rule.nodes) {
-      if (node.type === 'atrule' && node.name === 'apply') {
-        if (lastGroup.length > 0) {
-          nodeGroups.push(lastGroup)
-          lastGroup = []
-        }
-        nodeGroups.push([node])
-      } else {
-        lastGroup.push(node)
-      }
-    }
-
-    if (lastGroup.length > 0) {
-      nodeGroups.push(lastGroup)
-    }
-
-    if (nodeGroups.length === 1) {
-      continue
-    }
-
-    for (let group of [...nodeGroups].reverse()) {
-      let newParent = rule.clone({ nodes: [] })
-      newParent.append(group)
-      rule.after(newParent)
-    }
-
-    rule.remove()
-  }
-}
-
 function processApply(root, context) {
   let applyCandidates = new Set()
 
@@ -343,7 +302,6 @@ function processApply(root, context) {
 
 export default function expandApplyAtRules(context) {
   return (root) => {
-    partitionApplyParents(root)
     processApply(root, context)
   }
 }
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -20,6 +20,58 @@ import log from '../util/log'
 import negateValue from '../util/negateValue'
 import isValidArbitraryValue from '../util/isValidArbitraryValue'
 
+function partitionRules(root) {
+  if (!root.walkAtRules) return [root]
+
+  let applyParents = new Set()
+  let rules = []
+
+  root.walkAtRules('apply', (rule) => {
+    applyParents.add(rule.parent)
+  })
+
+  if (applyParents.size === 0) {
+    rules.push(root)
+  }
+
+  for (let rule of applyParents) {
+    let nodeGroups = []
+    let lastGroup = []
+
+    for (let node of rule.nodes) {
+      if (node.type === 'atrule' && node.name === 'apply') {
+        if (lastGroup.length > 0) {
+          nodeGroups.push(lastGroup)
+          lastGroup = []
+        }
+        nodeGroups.push([node])
+      } else {
+        lastGroup.push(node)
+      }
+    }
+
+    if (lastGroup.length > 0) {
+      nodeGroups.push(lastGroup)
+    }
+
+    if (nodeGroups.length === 1) {
+      rules.push(rule)
+      continue
+    }
+
+    for (let group of [...nodeGroups].reverse()) {
+      let clone = rule.clone({ nodes: [] })
+      clone.append(group)
+      rules.unshift(clone)
+      rule.after(clone)
+    }
+
+    rule.remove()
+  }
+
+  return rules
+}
+
 function parseVariantFormatString(input) {
   if (input.includes('{')) {
     if (!isBalanced(input)) throw new Error(`Your { and } are unbalanced.`)
@@ -232,7 +284,9 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
           context.candidateRuleMap.set(identifier, [])
         }
 
-        context.candidateRuleMap.get(identifier).push([{ sort: offset, layer: 'user' }, rule])
+        context.candidateRuleMap
+          .get(identifier)
+          .push(...partitionRules(rule).map((rule) => [{ sort: offset, layer: 'user' }, rule]))
       }
     },
     addBase(base) {
@@ -246,7 +300,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
         context.candidateRuleMap
           .get(prefixedIdentifier)
-          .push([{ sort: offset, layer: 'base' }, rule])
+          .push(...partitionRules(rule).map((rule) => [{ sort: offset, layer: 'base' }, rule]))
       }
     },
     /**
@@ -260,7 +314,6 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
       for (let [identifier, rule] of withIdentifiers(groups)) {
         let prefixedIdentifier = prefixIdentifier(identifier, {})
-        let offset = offsets.base++
 
         if (!context.candidateRuleMap.has(prefixedIdentifier)) {
           context.candidateRuleMap.set(prefixedIdentifier, [])
@@ -268,7 +321,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
         context.candidateRuleMap
           .get(prefixedIdentifier)
-          .push([{ sort: offset, layer: 'defaults' }, rule])
+          .push(
+            ...partitionRules(rule).map((rule) => [
+              { sort: offsets.base++, layer: 'defaults' },
+              rule,
+            ])
+          )
       }
     },
     addComponents(components, options) {
@@ -281,7 +339,6 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
       for (let [identifier, rule] of withIdentifiers(components)) {
         let prefixedIdentifier = prefixIdentifier(identifier, options)
-        let offset = offsets.components++
 
         classList.add(prefixedIdentifier)
 
@@ -291,7 +348,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
         context.candidateRuleMap
           .get(prefixedIdentifier)
-          .push([{ sort: offset, layer: 'components', options }, rule])
+          .push(
+            ...partitionRules(rule).map((rule) => [
+              { sort: offsets.components++, layer: 'components', options },
+              rule,
+            ])
+          )
       }
     },
     addUtilities(utilities, options) {
@@ -304,7 +366,6 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
       for (let [identifier, rule] of withIdentifiers(utilities)) {
         let prefixedIdentifier = prefixIdentifier(identifier, options)
-        let offset = offsets.utilities++
 
         classList.add(prefixedIdentifier)
 
@@ -314,7 +375,12 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
         context.candidateRuleMap
           .get(prefixedIdentifier)
-          .push([{ sort: offset, layer: 'utilities', options }, rule])
+          .push(
+            ...partitionRules(rule).map((rule) => [
+              { sort: offsets.utilities++, layer: 'utilities', options },
+              rule,
+            ])
+          )
       }
     },
     matchUtilities: function (utilities, options) {
diff --git a/src/processTailwindFeatures.js b/src/processTailwindFeatures.js
--- a/src/processTailwindFeatures.js
+++ b/src/processTailwindFeatures.js
@@ -14,6 +14,8 @@ export default function processTailwindFeatures(setupContext) {
   return function (root, result) {
     let { tailwindDirectives, applyDirectives } = normalizeTailwindDirectives(root)
 
+    detectNesting()(root, result)
+
     let context = setupContext({
       tailwindDirectives,
       applyDirectives,
@@ -37,7 +39,6 @@ export default function processTailwindFeatures(setupContext) {
 
     issueFlagNotices(context.tailwindConfig)
 
-    detectNesting(context)(root, result)
     expandTailwindAtRules(context)(root, result)
     expandApplyAtRules(context)(root, result)
     evaluateTailwindFunctions(context)(root, result)


Test patch:
diff --git a/tests/apply.test.css b/tests/apply.test.css
--- a/tests/apply.test.css
+++ b/tests/apply.test.css
@@ -122,7 +122,6 @@
     text-align: left;
   }
 }
-/* TODO: This works but the generated CSS is unnecessarily verbose. */
 .complex-utilities {
   --tw-ordinal: ordinal;
   --tw-numeric-spacing: tabular-nums;
@@ -144,14 +143,6 @@
   --tw-numeric-fraction: diagonal-fractions;
   font-variant-numeric: var(--tw-font-variant-numeric);
 }
-.basic-nesting-parent {
-  .basic-nesting-child {
-    font-weight: 700;
-  }
-  .basic-nesting-child:hover {
-    font-weight: 400;
-  }
-}
 .use-base-only-a {
   font-weight: 700;
 }
diff --git a/tests/apply.test.js b/tests/apply.test.js
--- a/tests/apply.test.js
+++ b/tests/apply.test.js
@@ -52,15 +52,9 @@ test('@apply', () => {
       .selectors-group {
         @apply group-hover:text-center lg:group-hover:text-left;
       }
-      /* TODO: This works but the generated CSS is unnecessarily verbose. */
       .complex-utilities {
         @apply ordinal tabular-nums focus:diagonal-fractions shadow-lg hover:shadow-xl;
       }
-      .basic-nesting-parent {
-        .basic-nesting-child {
-          @apply font-bold hover:font-normal;
-        }
-      }
       .use-base-only-a {
         @apply font-bold;
       }
@@ -910,6 +904,196 @@ it('should be possible to apply a class from another rule with multiple selector
   })
 })
 
+describe('multiple instances', () => {
+  it('should be possible to apply multiple "instances" of the same class', () => {
+    let config = {
+      content: [{ raw: html`` }],
+      plugins: [],
+      corePlugins: { preflight: false },
+    }
+
+    let input = css`
+      .a {
+        @apply b;
+      }
+
+      .b {
+        @apply uppercase;
+      }
+
+      .b {
+        color: red;
+      }
+    `
+
+    return run(input, config).then((result) => {
+      return expect(result.css).toMatchFormattedCss(css`
+        .a {
+          text-transform: uppercase;
+          color: red;
+        }
+
+        .b {
+          text-transform: uppercase;
+          color: red;
+        }
+      `)
+    })
+  })
+
+  it('should be possible to apply a combination of multiple "instances" of the same class', () => {
+    let config = {
+      content: [{ raw: html`` }],
+      plugins: [],
+      corePlugins: { preflight: false },
+    }
+
+    let input = css`
+      .a {
+        @apply b;
+      }
+
+      .b {
+        @apply uppercase;
+        color: red;
+      }
+    `
+
+    return run(input, config).then((result) => {
+      return expect(result.css).toMatchFormattedCss(css`
+        .a {
+          text-transform: uppercase;
+          color: red;
+        }
+
+        .b {
+          text-transform: uppercase;
+          color: red;
+        }
+      `)
+    })
+  })
+
+  it('should generate the same output, even if it was used in a @layer', () => {
+    let config = {
+      content: [{ raw: html`<div class="a b"></div>` }],
+      plugins: [],
+      corePlugins: { preflight: false },
+    }
+
+    let input = css`
+      @tailwind components;
+
+      @layer components {
+        .a {
+          @apply b;
+        }
+
+        .b {
+          @apply uppercase;
+          color: red;
+        }
+      }
+    `
+
+    return run(input, config).then((result) => {
+      return expect(result.css).toMatchFormattedCss(css`
+        .a {
+          text-transform: uppercase;
+          color: red;
+        }
+
+        .b {
+          text-transform: uppercase;
+          color: red;
+        }
+      `)
+    })
+  })
+
+  it('should be possible to apply a combination of multiple "instances" of the same class (defined in a layer)', () => {
+    let config = {
+      content: [{ raw: html`<div class="a b"></div>` }],
+      plugins: [],
+      corePlugins: { preflight: false },
+    }
+
+    let input = css`
+      @tailwind components;
+
+      @layer components {
+        .a {
+          color: red;
+          @apply b;
+          color: blue;
+        }
+
+        .b {
+          @apply text-green-500;
+          text-decoration: underline;
+        }
+      }
+    `
+
+    return run(input, config).then((result) => {
+      return expect(result.css).toMatchFormattedCss(css`
+        .a {
+          color: red;
+          --tw-text-opacity: 1;
+          color: rgb(34 197 94 / var(--tw-text-opacity));
+          text-decoration: underline;
+          color: blue;
+        }
+
+        .b {
+          --tw-text-opacity: 1;
+          color: rgb(34 197 94 / var(--tw-text-opacity));
+          text-decoration: underline;
+        }
+      `)
+    })
+  })
+
+  it('should properly maintain the order', () => {
+    let config = {
+      content: [{ raw: html`` }],
+      plugins: [],
+      corePlugins: { preflight: false },
+    }
+
+    let input = css`
+      h2 {
+        @apply text-xl;
+        @apply lg:text-3xl;
+        @apply sm:text-2xl;
+      }
+    `
+
+    return run(input, config).then((result) => {
+      return expect(result.css).toMatchFormattedCss(css`
+        h2 {
+          font-size: 1.25rem;
+          line-height: 1.75rem;
+        }
+
+        @media (min-width: 1024px) {
+          h2 {
+            font-size: 1.875rem;
+            line-height: 2.25rem;
+          }
+        }
+
+        @media (min-width: 640px) {
+          h2 {
+            font-size: 1.5rem;
+            line-height: 2rem;
+          }
+        }
+      `)
+    })
+  })
+})
+
 /*
 it('apply can emit defaults in isolated environments without @tailwind directives', () => {
   let config = {