Repository: tailwindlabs/tailwindcss
Pull request #5378 (2021-09-03 11:50:54)

Support `@apply` for classes outside of a `@layer`
This PR will allow you to apply classes that are not inside a `@layer` directive. This is an important part of making JIT the default for backwards compatibility reasons. In short:

The following already worked:
```css
@tailwind utilities;

.foo {
  @apply bg-red-500 hover:bg-red-600;
}
```

This got converted to the following output:
```css
.foo {
  --tw-bg-opacity: 1;
  background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
}
.foo:hover {
   --tw-bg-opacity: 1;
  background-color: rgba(220, 38, 38, var(--tw-bg-opacity));
}
```

However, it was not possible to do this:

```css
@tailwind utilities;

.foo {
  @apply bg-red-500 hover:bg-red-600;
}

.bar {
  @apply hover:foo;
}
```

This resulted in the following output:
```
CssSyntaxError: The `hover:foo` class does not exist. If `hover:foo` is a custom class, make sure it is defined within a `@layer` directive.
```

But with this PR, it results in the following output:
```css
.foo {
  --tw-bg-opacity: 1;
  background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
}
.foo:hover {
  --tw-bg-opacity: 1;
  background-color: rgba(220, 38, 38, var(--tw-bg-opacity));
}
.bar:hover {
  --tw-bg-opacity: 1;
  background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
}
.bar:hover:hover {
  --tw-bg-opacity: 1;
  background-color: rgba(220, 38, 38, var(--tw-bg-opacity));
}
```

There is this weird issue that `.bar:hover:hover` exists, but I will fix that in another PR. (Note: this is an existing issue, and not a newly introduced issue)

---

Apart from the backwards compatibility aspect, the biggest difference is the behaviour whether you are using the class or not.

```css
@tailwind utilities;

.foo { /* This will _always_ exist in your output, even if you didn't use the "foo" class in your content. */
  
}

@layer components {
  .bar { /* This will _only_ exist in your output if you used the "bar" class in your content. */
  }
}
```



Patch:
diff --git a/src/lib/expandApplyAtRules.js b/src/lib/expandApplyAtRules.js
index e9de7713..ed3caa13 100644
--- a/src/lib/expandApplyAtRules.js
+++ b/src/lib/expandApplyAtRules.js
@@ -97,6 +97,7 @@ function processApply(root, context) {
     for (let util of candidates) {
       applyCandidates.add(util)
     }
+
     applies.push(rule)
   })
 
@@ -210,7 +211,12 @@ function processApply(root, context) {
             })
           }
 
-          siblings.push([meta, root.nodes[0]])
+          // Insert it
+          siblings.push([
+            // Ensure that when we are sorting, that we take the layer order into account
+            { ...meta, sort: meta.sort | context.layerOrder[meta.layer] },
+            root.nodes[0],
+          ])
         }
       }
 
diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js
index 747ae145..c046bea8 100644
--- a/src/lib/expandTailwindAtRules.js
+++ b/src/lib/expandTailwindAtRules.js
@@ -109,6 +109,16 @@ function buildStylesheet(rules, context) {
     components: new Set(),
     utilities: new Set(),
     variants: new Set(),
+
+    // All the CSS that is not Tailwind related can be put in this bucket. This
+    // will make it easier to later use this information when we want to
+    // `@apply` for example. The main reason we do this here is because we
+    // still need to make sure the order is correct. Last but not least, we
+    // will make sure to always re-inject this section into the css, even if
+    // certain rules were not used. This means that it will look like a no-op
+    // from the user's perspective, but we gathered all the useful information
+    // we need.
+    user: new Set(),
   }
 
   for (let [sort, rule] of sortedRules) {
@@ -131,6 +141,11 @@ function buildStylesheet(rules, context) {
       returnValue.utilities.add(rule)
       continue
     }
+
+    if (sort & context.layerOrder.user) {
+      returnValue.user.add(rule)
+      continue
+    }
   }
 
   return returnValue
diff --git a/src/lib/setupContextUtils.js b/src/lib/setupContextUtils.js
index c6a9732a..9c37f968 100644
--- a/src/lib/setupContextUtils.js
+++ b/src/lib/setupContextUtils.js
@@ -201,6 +201,17 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
 
       return getConfigValue(['variants', path], defaultValue)
     },
+    addUserCss(userCss) {
+      for (let [identifier, rule] of withIdentifiers(userCss)) {
+        let offset = offsets.user++
+
+        if (!context.candidateRuleMap.has(identifier)) {
+          context.candidateRuleMap.set(identifier, [])
+        }
+
+        context.candidateRuleMap.get(identifier).push([{ sort: offset, layer: 'user' }, rule])
+      }
+    },
     addBase(base) {
       for (let [identifier, rule] of withIdentifiers(base)) {
         let prefixedIdentifier = prefixIdentifier(identifier, {})
@@ -404,6 +415,15 @@ function collectLayerPlugins(root) {
     }
   })
 
+  root.walkRules((rule) => {
+    // At this point it is safe to include all the left-over css from the
+    // user's css file. This is because the `@tailwind` and `@layer` directives
+    // will already be handled and will be removed from the css tree.
+    layerPlugins.push(function ({ addUserCss }) {
+      addUserCss(rule, { respectPrefix: false })
+    })
+  })
+
   return layerPlugins
 }
 
@@ -448,6 +468,7 @@ function registerPlugins(plugins, context) {
     base: 0n,
     components: 0n,
     utilities: 0n,
+    user: 0n,
   }
 
   let pluginApi = buildPluginApi(context.tailwindConfig, context, {
@@ -470,6 +491,7 @@ function registerPlugins(plugins, context) {
     offsets.base,
     offsets.components,
     offsets.utilities,
+    offsets.user,
   ])
   let reservedBits = BigInt(highestOffset.toString(2).length)
 
@@ -477,9 +499,10 @@ function registerPlugins(plugins, context) {
     base: (1n << reservedBits) << 0n,
     components: (1n << reservedBits) << 1n,
     utilities: (1n << reservedBits) << 2n,
+    user: (1n << reservedBits) << 3n,
   }
 
-  reservedBits += 3n
+  reservedBits += 4n
 
   let offset = 0
   context.variantOrder = new Map(


Test patch:
diff --git a/tests/apply.test.js b/tests/apply.test.js
index 0c96c273..182e0214 100644
--- a/tests/apply.test.js
+++ b/tests/apply.test.js
@@ -1,7 +1,7 @@
 import fs from 'fs'
 import path from 'path'
 
-import { run, css } from './util/run'
+import { run, html, css } from './util/run'
 
 test('@apply', () => {
   let config = {
@@ -236,7 +236,7 @@ test('@apply error when using a prefixed .group utility', async () => {
   let config = {
     prefix: 'tw-',
     darkMode: 'class',
-    content: [{ raw: '<div class="foo"></div>' }],
+    content: [{ raw: html`<div class="foo"></div>` }],
   }
 
   let input = css`
@@ -254,3 +254,151 @@ test('@apply error when using a prefixed .group utility', async () => {
     `@apply should not be used with the 'tw-group' utility`
   )
 })
+
+test('@apply classes from outside a @layer', async () => {
+  let config = {
+    content: [{ raw: html`<div class="font-bold foo bar baz"></div>` }],
+  }
+
+  let input = css`
+    @tailwind components;
+    @tailwind utilities;
+
+    .foo {
+      @apply font-bold;
+    }
+
+    .bar {
+      @apply foo text-red-500 hover:text-green-500;
+    }
+
+    .baz {
+      @apply bar underline;
+    }
+
+    .keep-me-even-though-I-am-not-used-in-content {
+      color: green;
+    }
+  `
+
+  await run(input, config).then((result) => {
+    return expect(result.css).toMatchFormattedCss(css`
+      .font-bold {
+        font-weight: 700;
+      }
+
+      .foo {
+        font-weight: 700;
+      }
+
+      .bar {
+        --tw-text-opacity: 1;
+        color: rgba(239, 68, 68, var(--tw-text-opacity));
+        font-weight: 700;
+      }
+
+      .bar:hover {
+        --tw-text-opacity: 1;
+        color: rgba(34, 197, 94, var(--tw-text-opacity));
+      }
+
+      .baz {
+        text-decoration: underline;
+        --tw-text-opacity: 1;
+        color: rgba(239, 68, 68, var(--tw-text-opacity));
+        font-weight: 700;
+      }
+
+      .baz:hover {
+        --tw-text-opacity: 1;
+        color: rgba(34, 197, 94, var(--tw-text-opacity));
+      }
+
+      .keep-me-even-though-I-am-not-used-in-content {
+        color: green;
+      }
+    `)
+  })
+})
+
+test('@applying classes from outside a @layer respects the source order', async () => {
+  let config = {
+    content: [{ raw: html`<div class="container font-bold foo bar baz"></div>` }],
+  }
+
+  let input = css`
+    .baz {
+      @apply bar underline;
+    }
+
+    @tailwind components;
+
+    .keep-me-even-though-I-am-not-used-in-content {
+      color: green;
+    }
+
+    @tailwind utilities;
+
+    .foo {
+      @apply font-bold;
+    }
+
+    .bar {
+      @apply no-underline;
+    }
+  `
+
+  await run(input, config).then((result) => {
+    return expect(result.css).toMatchFormattedCss(css`
+      .baz {
+        text-decoration: underline;
+        text-decoration: none;
+      }
+
+      .container {
+        width: 100%;
+      }
+      @media (min-width: 640px) {
+        .container {
+          max-width: 640px;
+        }
+      }
+      @media (min-width: 768px) {
+        .container {
+          max-width: 768px;
+        }
+      }
+      @media (min-width: 1024px) {
+        .container {
+          max-width: 1024px;
+        }
+      }
+      @media (min-width: 1280px) {
+        .container {
+          max-width: 1280px;
+        }
+      }
+      @media (min-width: 1536px) {
+        .container {
+          max-width: 1536px;
+        }
+      }
+
+      .keep-me-even-though-I-am-not-used-in-content {
+        color: green;
+      }
+
+      .font-bold {
+        font-weight: 700;
+      }
+
+      .foo {
+        font-weight: 700;
+      }
+
+      .bar {
+        text-decoration: none;
+      }
+    `)
+  })
+})