Repository: tailwindlabs/tailwindcss-jit (js)

Issue:
`@apply` doesn't respect order of sibling declarations within same rule
There are times where you still want to use good old CSS and previously I would add these under my `@apply` statements, and they'd be added to the bottom of the built css file and therefor override tailwind, but with jit they are being added to the top. eg:

**Disclaimer: I know I can do this manual css using tailwinds `bg-opacity-50`  it's just an example...**

## Before JIT
### app.sass
```
.some-div
  @apply bg-blue flex
  background: rgba(0,0,0,0.5)
```
### app.css (Built)
```
.some-div
  background-color: blue
  display: flex
  background-color: rgba(0,0,0,0.5)
```

## After JIT
### app.sass
```
.some-div
  @apply bg-blue flex
  background: rgba(0,0,0,0.5)
```
### app.css (Built)
```
.some-div
  background-color: rgba(0,0,0,0.5)
  background-color: blue
  display: flex
```


Pull request description:
Respect apply sibling order
Resolves #35 and makes complex `@apply` situations behave identically to regular Tailwind.

Patch:
diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js
index b969074..5124e21 100644
--- a/src/lib/expandApplyAtRules.js
+++ b/src/lib/expandApplyAtRules.js
@@ -40,135 +40,182 @@ function extractApplyCandidates(params) {
   return [candidates, false]
 }
 
-function expandApplyAtRules(context) {
-  return function processApply(root) {
-    let applyCandidates = new Set()
+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)
+      }
+    }
 
-    // Collect all @apply rules and candidates
-    let applies = []
-    root.walkAtRules('apply', (rule) => {
-      let [candidates, important] = extractApplyCandidates(rule.params)
+    if (lastGroup.length > 0) {
+      nodeGroups.push(lastGroup)
+    }
 
-      for (let util of candidates) {
-        applyCandidates.add(util)
-      }
-      applies.push(rule)
-    })
-
-    // Start the @apply process if we have rules with @apply in them
-    if (applies.length > 0) {
-      // Fill up some caches!
-      let applyClassCache = buildApplyCache(applyCandidates, context)
-
-      /**
-       * When we have an apply like this:
-       *
-       * .abc {
-       *    @apply hover:font-bold;
-       * }
-       *
-       * What we essentially will do is resolve to this:
-       *
-       * .abc {
-       *    @apply .hover\:font-bold:hover {
-       *      font-weight: 500;
-       *    }
-       * }
-       *
-       * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
-       * What happens in this function is that we prepend a `.` and escape the candidate.
-       * This will result in `.hover\:font-bold`
-       * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
-       */
-      // TODO: Should we use postcss-selector-parser for this instead?
-      function replaceSelector(selector, utilitySelectors, candidate) {
-        let needle = `.${escapeClassName(candidate)}`
-        let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g)
-
-        return selector
-          .split(/\s*,\s*/g)
-          .map((s) => {
-            let replaced = []
-
-            for (let utilitySelector of utilitySelectorsList) {
-              let replacedSelector = utilitySelector.replace(needle, s)
-              if (replacedSelector === utilitySelector) {
-                continue
-              }
-              replaced.push(replacedSelector)
-            }
-            return replaced.join(', ')
-          })
-          .join(', ')
-      }
+    if (nodeGroups.length === 1) {
+      continue
+    }
 
-      /** @type {Map<import('postcss').Node, [string, boolean, import('postcss').Node[]][]>} */
-      let perParentApplies = new Map()
+    for (let group of [...nodeGroups].reverse()) {
+      let newParent = rule.clone({ nodes: [] })
+      newParent.append(group)
+      rule.after(newParent)
+    }
 
-      // Collect all apply candidates and their rules
-      for (let apply of applies) {
-        let candidates = perParentApplies.get(apply.parent) || []
+    rule.remove()
+  }
+}
 
-        perParentApplies.set(apply.parent, candidates)
+function processApply(root, context) {
+  let applyCandidates = new Set()
 
-        let [applyCandidates, important] = extractApplyCandidates(apply.params)
+  // Collect all @apply rules and candidates
+  let applies = []
+  root.walkAtRules('apply', (rule) => {
+    let [candidates] = extractApplyCandidates(rule.params)
 
-        for (let applyCandidate of applyCandidates) {
-          if (!applyClassCache.has(applyCandidate)) {
-            throw apply.error(
-              `The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
-            )
+    for (let util of candidates) {
+      applyCandidates.add(util)
+    }
+    applies.push(rule)
+  })
+
+  // Start the @apply process if we have rules with @apply in them
+  if (applies.length > 0) {
+    // Fill up some caches!
+    let applyClassCache = buildApplyCache(applyCandidates, context)
+
+    /**
+     * When we have an apply like this:
+     *
+     * .abc {
+     *    @apply hover:font-bold;
+     * }
+     *
+     * What we essentially will do is resolve to this:
+     *
+     * .abc {
+     *    @apply .hover\:font-bold:hover {
+     *      font-weight: 500;
+     *    }
+     * }
+     *
+     * Notice that the to-be-applied class is `.hover\:font-bold:hover` and that the utility candidate was `hover:font-bold`.
+     * What happens in this function is that we prepend a `.` and escape the candidate.
+     * This will result in `.hover\:font-bold`
+     * Which means that we can replace `.hover\:font-bold` with `.abc` in `.hover\:font-bold:hover` resulting in `.abc:hover`
+     */
+    // TODO: Should we use postcss-selector-parser for this instead?
+    function replaceSelector(selector, utilitySelectors, candidate) {
+      let needle = `.${escapeClassName(candidate)}`
+      let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g)
+
+      return selector
+        .split(/\s*,\s*/g)
+        .map((s) => {
+          let replaced = []
+
+          for (let utilitySelector of utilitySelectorsList) {
+            let replacedSelector = utilitySelector.replace(needle, s)
+            if (replacedSelector === utilitySelector) {
+              continue
+            }
+            replaced.push(replacedSelector)
           }
+          return replaced.join(', ')
+        })
+        .join(', ')
+    }
+
+    /** @type {Map<import('postcss').Node, [string, boolean, import('postcss').Node[]][]>} */
+    let perParentApplies = new Map()
+
+    // Collect all apply candidates and their rules
+    for (let apply of applies) {
+      let candidates = perParentApplies.get(apply.parent) || []
 
-          let rules = applyClassCache.get(applyCandidate)
+      perParentApplies.set(apply.parent, candidates)
 
-          candidates.push([applyCandidate, important, rules])
+      let [applyCandidates, important] = extractApplyCandidates(apply.params)
+
+      for (let applyCandidate of applyCandidates) {
+        if (!applyClassCache.has(applyCandidate)) {
+          throw apply.error(
+            `The \`${applyCandidate}\` class does not exist. If \`${applyCandidate}\` is a custom class, make sure it is defined within a \`@layer\` directive.`
+          )
         }
+
+        let rules = applyClassCache.get(applyCandidate)
+
+        candidates.push([applyCandidate, important, rules])
       }
+    }
 
-      for (const [parent, candidates] of perParentApplies) {
-        let siblings = []
+    for (const [parent, candidates] of perParentApplies) {
+      let siblings = []
 
-        for (let [applyCandidate, important, rules] of candidates) {
-          for (let [meta, node] of rules) {
-            let root = postcss.root({ nodes: [node.clone()] })
-            let canRewriteSelector =
-              node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
+      for (let [applyCandidate, important, rules] of candidates) {
+        for (let [meta, node] of rules) {
+          let root = postcss.root({ nodes: [node.clone()] })
+          let canRewriteSelector =
+            node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
 
-            if (canRewriteSelector) {
-              root.walkRules((rule) => {
-                rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate)
+          if (canRewriteSelector) {
+            root.walkRules((rule) => {
+              rule.selector = replaceSelector(parent.selector, rule.selector, applyCandidate)
 
-                rule.walkDecls((d) => {
-                  d.important = important
-                })
+              rule.walkDecls((d) => {
+                d.important = important
               })
-            }
-
-            siblings.push([meta, root.nodes[0]])
+            })
           }
+
+          siblings.push([meta, root.nodes[0]])
         }
+      }
 
-        // Inject the rules, sorted, correctly
-        const nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
+      // Inject the rules, sorted, correctly
+      let nodes = siblings.sort(([a], [z]) => bigSign(a.sort - z.sort)).map((s) => s[1])
 
-        // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
-        parent.after(nodes)
-      }
+      // console.log(parent)
+      // `parent` refers to the node at `.abc` in: .abc { @apply mt-2 }
+      parent.after(nodes)
+    }
 
-      for (let apply of applies) {
-        // If there are left-over declarations, just remove the @apply
-        if (apply.parent.nodes.length > 1) {
-          apply.remove()
-        } else {
-          // The node is empty, drop the full node
-          apply.parent.remove()
-        }
+    for (let apply of applies) {
+      // If there are left-over declarations, just remove the @apply
+      if (apply.parent.nodes.length > 1) {
+        apply.remove()
+      } else {
+        // The node is empty, drop the full node
+        apply.parent.remove()
       }
-
-      // Do it again, in case we have other `@apply` rules
-      processApply(root)
     }
+
+    // Do it again, in case we have other `@apply` rules
+    processApply(root, context)
+  }
+}
+
+function expandApplyAtRules(context) {
+  return (root) => {
+    partitionApplyParents(root)
+    processApply(root, context)
   }
 }
 


Test patch:
diff --git a/tests/10-apply.test.css b/tests/10-apply.test.css
index dd7ee96..60b6f08 100644
--- a/tests/10-apply.test.css
+++ b/tests/10-apply.test.css
@@ -265,6 +265,32 @@
   color: green;
   font-weight: 700;
 }
+.add-sibling-properties {
+  padding: 2rem;
+  padding-left: 1rem;
+  padding-right: 1rem;
+}
+.add-sibling-properties:hover {
+  padding-left: 0.5rem;
+  padding-right: 0.5rem;
+}
+@media (min-width: 1024px) {
+  .add-sibling-properties {
+    padding-left: 2.5rem;
+    padding-right: 2.5rem;
+  }
+}
+@media (min-width: 1280px) {
+  .add-sibling-properties:focus {
+    padding-left: 0.25rem;
+    padding-right: 0.25rem;
+  }
+}
+.add-sibling-properties {
+  padding-top: 3px;
+  color: green;
+  font-weight: 700;
+}
 h1 {
   font-size: 1.5rem;
   line-height: 2rem;
@@ -285,13 +311,13 @@ h2 {
   font-size: 1.5rem;
   line-height: 2rem;
 }
-@media (min-width: 640px) {
+@media (min-width: 1024px) {
   h2 {
     font-size: 1.5rem;
     line-height: 2rem;
   }
 }
-@media (min-width: 1024px) {
+@media (min-width: 640px) {
   h2 {
     font-size: 1.5rem;
     line-height: 2rem;
diff --git a/tests/10-apply.test.html b/tests/10-apply.test.html
index b63cc4b..4cffa15 100644
--- a/tests/10-apply.test.html
+++ b/tests/10-apply.test.html
@@ -31,6 +31,7 @@
     <div class="recursive-apply-b"></div>
     <div class="recursive-apply-c"></div>
     <div class="use-with-other-properties-base use-with-other-properties-component"></div>
+    <div class="add-sibling-properties"></div>
     <div class="a b"></div>
     <div class="foo"></div>
     <div class="bar"></div>
diff --git a/tests/10-apply.test.js b/tests/10-apply.test.js
index a9fa533..d513cd1 100644
--- a/tests/10-apply.test.js
+++ b/tests/10-apply.test.js
@@ -78,7 +78,7 @@ test('@apply', () => {
       @apply use-dependant-only-a font-normal;
     }
     .btn {
-      @apply font-bold py-2 px-4 rounded; 
+      @apply font-bold py-2 px-4 rounded;
     }
     .btn-blue {
       @apply btn bg-blue-500 hover:bg-blue-700 text-white;
@@ -99,6 +99,12 @@ test('@apply', () => {
     .use-with-other-properties-component {
       @apply use-with-other-properties-base;
     }
+    .add-sibling-properties {
+      padding: 2rem;
+      @apply px-4 hover:px-2 lg:px-10 xl:focus:px-1;
+      padding-top: 3px;
+      @apply use-with-other-properties-base;
+    }
 
     h1 {
       @apply text-2xl lg:text-2xl sm:text-3xl;