Repository: tailwindlabs/tailwindcss-jit
Pull request #136 (2021-03-22 11:22:38)

Recursive apply
Add a naive implementation of the recursive `@apply`.
We can improve this if this causes actual issues.

For example, currently in the tests are are running through the plugin 4 times.
If we create a dependency graph instead, then we could do a topological sort so that we only have to loop through the apply plugin once.
This improvement will also make it easier to detect cycles (currently we would loop forever).

Fixes: #66


Patch:
diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js
index c3bad3e..d0a46fe 100644
--- a/src/lib/expandApplyAtRules.js
+++ b/src/lib/expandApplyAtRules.js
@@ -9,7 +9,10 @@ function buildApplyCache(applyCandidates, context) {
     }
 
     if (context.classCache.has(candidate)) {
-      context.applyClassCache.set(candidate, context.classCache.get(candidate))
+      context.applyClassCache.set(
+        candidate,
+        context.classCache.get(candidate).map(([meta, rule]) => [meta, rule.clone()])
+      )
       continue
     }
 
@@ -38,7 +41,7 @@ function extractApplyCandidates(params) {
 }
 
 function expandApplyAtRules(context) {
-  return (root) => {
+  return function processApply(root) {
     let applyCandidates = new Set()
 
     // Collect all @apply rules and candidates
@@ -79,12 +82,16 @@ function expandApplyAtRules(context) {
        */
       // 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 utilitySelectors.split(/\s*,\s*/g)) {
-              let replacedSelector = utilitySelector.replace(`.${escapeClassName(candidate)}`, s)
+
+            for (let utilitySelector of utilitySelectorsList) {
+              let replacedSelector = utilitySelector.replace(needle, s)
               if (replacedSelector === utilitySelector) {
                 continue
               }
@@ -136,6 +143,9 @@ function expandApplyAtRules(context) {
           apply.parent.remove()
         }
       }
+
+      // Do it again, in case we have other `@apply` rules
+      processApply(root)
     }
   }
 }


Test patch:
diff --git a/tests/10-apply.test.css b/tests/10-apply.test.css
index ceca6f3..ddf73cf 100644
--- a/tests/10-apply.test.css
+++ b/tests/10-apply.test.css
@@ -178,3 +178,90 @@
     font-weight: 400;
   }
 }
+.use-base-only-a {
+  font-weight: 700;
+}
+.use-dependant-only-b {
+  font-weight: 700;
+  font-weight: 400;
+}
+.btn {
+  border-radius: 0.25rem;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+  font-weight: 700;
+}
+.btn-blue {
+  border-radius: 0.25rem;
+  padding-left: 1rem;
+  padding-right: 1rem;
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
+  font-weight: 700;
+  --tw-bg-opacity: 1;
+  background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
+  --tw-text-opacity: 1;
+  color: rgba(255, 255, 255, var(--tw-text-opacity));
+}
+.btn-blue:hover {
+  --tw-bg-opacity: 1;
+  background-color: rgba(29, 78, 216, var(--tw-bg-opacity));
+}
+.recursive-apply-a {
+  font-weight: 900;
+}
+@media (min-width: 640px) {
+  .recursive-apply-a {
+    font-weight: 100;
+  }
+}
+.recursive-apply-b {
+  font-weight: 900;
+}
+@media (min-width: 640px) {
+  .recursive-apply-b {
+    font-weight: 100;
+  }
+}
+.recursive-apply-b {
+  font-weight: 600;
+}
+@media (min-width: 768px) {
+  .recursive-apply-b {
+    font-weight: 200;
+  }
+}
+.recursive-apply-c {
+  font-weight: 900;
+}
+@media (min-width: 640px) {
+  .recursive-apply-c {
+    font-weight: 100;
+  }
+}
+.recursive-apply-c {
+  font-weight: 600;
+}
+@media (min-width: 768px) {
+  .recursive-apply-c {
+    font-weight: 200;
+  }
+}
+.recursive-apply-c {
+  font-weight: 700;
+}
+@media (min-width: 1024px) {
+  .recursive-apply-c {
+    font-weight: 300;
+  }
+}
+.use-with-other-properties-base {
+  color: green;
+  font-weight: 700;
+}
+.use-with-other-properties-component {
+  color: green;
+  font-weight: 700;
+}
diff --git a/tests/10-apply.test.html b/tests/10-apply.test.html
index 7f9c472..d355412 100644
--- a/tests/10-apply.test.html
+++ b/tests/10-apply.test.html
@@ -23,5 +23,14 @@
     <div class="basic-nesting-parent">
       <div class="basic-nesting-child"></div>
     </div>
+    <div class="use-base-only-a"></div>
+    <div class="use-dependant-only-b"></div>
+    <div class="btn"></div>
+    <div class="btn-blue"></div>
+    <div class="recursive-apply-a"></div>
+    <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="a b"></div>
   </body>
 </html>
diff --git a/tests/10-apply.test.js b/tests/10-apply.test.js
index 173f190..732ed03 100644
--- a/tests/10-apply.test.js
+++ b/tests/10-apply.test.js
@@ -65,6 +65,40 @@ test('@apply', () => {
         @apply font-bold hover:font-normal;
       }
     }
+    .use-base-only-a {
+      @apply font-bold;
+    }
+    .use-base-only-b {
+      @apply use-base-only-a font-normal;
+    }
+    .use-dependant-only-a {
+      @apply font-bold;
+    }
+    .use-dependant-only-b {
+      @apply use-dependant-only-a font-normal;
+    }
+    .btn {
+      @apply font-bold py-2 px-4 rounded; 
+    }
+    .btn-blue {
+      @apply btn bg-blue-500 hover:bg-blue-700 text-white;
+    }
+    .recursive-apply-a {
+      @apply font-black sm:font-thin;
+    }
+    .recursive-apply-b {
+      @apply recursive-apply-a font-semibold md:font-extralight;
+    }
+    .recursive-apply-c {
+      @apply recursive-apply-b font-bold lg:font-light;
+    }
+    .use-with-other-properties-base {
+      color: green;
+      @apply font-bold;
+    }
+    .use-with-other-properties-component {
+      @apply use-with-other-properties-base;
+    }
   }
 
   @layer utilities {