Repository: tailwindlabs/tailwindcss
Pull request #7066 (2022-01-14 15:12:51)

Only emit utility/component variants when those layers exist
We were previously always emitting a variants layer even if you didn't include `@tailwind utilities` or `@tailwind variants` this  means we would previously emit variants even in a file that included only `@tailwind base;`. This has been fixed.

Also, previously we would generate variants like `sm:container` even when you didn't have `@tailwind components` in your file. We now only emit variants for utilities/components when you have `@tailwind utilities`/`@tailwind components` included.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c1bccab5..f5b16dbf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Show warnings for invalid content config ([#7065](https://github.com/tailwindlabs/tailwindcss/pull/7065))
 
+### Fixed
+
+- Only emit utility/component variants when those layers exist ([#7066](https://github.com/tailwindlabs/tailwindcss/pull/7066))
+
 ## [3.0.13] - 2022-01-11
 
 ### Fixed
diff --git a/nesting/plugin.js b/nesting/plugin.js
index 72bb24c2..2b558664 100644
--- a/nesting/plugin.js
+++ b/nesting/plugin.js
@@ -14,7 +14,10 @@ module.exports = function nesting(opts = postcssNested) {
     })
 
     let plugin = (() => {
-      if (typeof opts === 'function') {
+      if (
+        typeof opts === 'function' ||
+        (typeof opts === 'object' && opts?.hasOwnProperty('postcssPlugin'))
+      ) {
         return opts
       }
 
diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js
index b24fe527..fc073834 100644
--- a/src/lib/expandTailwindAtRules.js
+++ b/src/lib/expandTailwindAtRules.js
@@ -205,9 +205,6 @@ export default function expandTailwindAtRules(context) {
 
     if (layerNodes.base) {
       layerNodes.base.before(cloneNodes([...baseNodes, ...defaultNodes], layerNodes.base.source))
-    }
-
-    if (layerNodes.base) {
       layerNodes.base.remove()
     }
 
@@ -221,16 +218,34 @@ export default function expandTailwindAtRules(context) {
       layerNodes.utilities.remove()
     }
 
+    // We do post-filtering to not alter the emitted order of the variants
+    const variantNodes = Array.from(screenNodes).filter((node) => {
+      const parentLayer = node.raws.tailwind?.parentLayer
+
+      if (parentLayer === 'components') {
+        return layerNodes.components !== null
+      }
+
+      if (parentLayer === 'utilities') {
+        return layerNodes.utilities !== null
+      }
+
+      return true
+    })
+
     if (layerNodes.variants) {
-      layerNodes.variants.before(cloneNodes([...screenNodes], layerNodes.variants.source))
+      layerNodes.variants.before(cloneNodes(variantNodes, layerNodes.variants.source))
       layerNodes.variants.remove()
-    } else {
-      root.append(cloneNodes([...screenNodes], root.source))
+    } else if (variantNodes.length > 0) {
+      root.append(cloneNodes(variantNodes, root.source))
     }
 
     // If we've got a utility layer and no utilities are generated there's likely something wrong
-    // TODO: Detect utility variants only
-    if (layerNodes.utilities && utilityNodes.size === 0 && screenNodes.size === 0) {
+    const hasUtilityVariants = variantNodes.some(
+      (node) => node.raws.tailwind?.parentLayer === 'utilities'
+    )
+
+    if (layerNodes.utilities && utilityNodes.size === 0 && !hasUtilityVariants) {
       log.warn('content-problems', [
         'No utilities were generated there is likely a problem with the `content` key in the tailwind config. For more information see the documentation: https://tailwindcss.com/docs/content-configuration',
       ])
diff --git a/src/lib/generateRules.js b/src/lib/generateRules.js
index 150bf685..7eb80fdc 100644
--- a/src/lib/generateRules.js
+++ b/src/lib/generateRules.js
@@ -216,6 +216,12 @@ function applyVariant(variant, matches, context) {
           })
         }
 
+        // This tracks the originating layer for the variant
+        // For example:
+        // .sm:underline {} is a variant of something in the utilities layer
+        // .sm:container {} is a variant of the container component
+        clone.nodes[0].raws.tailwind = { parentLayer: meta.layer }
+
         let withOffset = [
           {
             ...meta,


Test patch:
diff --git a/tests/postcss-plugins/nesting/index.test.js b/tests/postcss-plugins/nesting/index.test.js
index 03f184de..2fcfb731 100644
--- a/tests/postcss-plugins/nesting/index.test.js
+++ b/tests/postcss-plugins/nesting/index.test.js
@@ -74,7 +74,7 @@ it('should default to the bundled postcss-nested plugin (no options)', async ()
   `)
 })
 
-it('should default to the bundled postcss-nested plugin (empty ooptions)', async () => {
+it('should default to the bundled postcss-nested plugin (empty options)', async () => {
   let input = css`
     .foo {
       color: black;
@@ -97,6 +97,29 @@ it('should default to the bundled postcss-nested plugin (empty ooptions)', async
   `)
 })
 
+it('should be possible to use postcss-nested plugin with options', async () => {
+  let input = css`
+    .foo {
+      color: black;
+      @screen md {
+        color: blue;
+      }
+    }
+  `
+
+  expect(await run(input, postcssNested({ noIsPseudoSelector: true }))).toMatchCss(css`
+    .foo {
+      color: black;
+    }
+
+    @media screen(md) {
+      .foo {
+        color: blue;
+      }
+    }
+  `)
+})
+
 test('@screen rules are replaced with media queries', async () => {
   let input = css`
     .foo {
diff --git a/tests/variants.test.js b/tests/variants.test.js
index 058dd552..72defcfe 100644
--- a/tests/variants.test.js
+++ b/tests/variants.test.js
@@ -1,7 +1,7 @@
 import fs from 'fs'
 import path from 'path'
 
-import { run, css, html } from './util/run'
+import { run, css, html, defaults } from './util/run'
 
 test('variants', () => {
   let config = {
@@ -467,3 +467,75 @@ it('should be possible to use responsive modifiers that are defined with special
     `)
   })
 })
+
+it('including just the base layer should not produce variants', () => {
+  let config = {
+    content: [{ raw: html`<div class="sm:container sm:underline"></div>` }],
+    corePlugins: { preflight: false },
+  }
+
+  return run('@tailwind base', config).then((result) => {
+    return expect(result.css).toMatchFormattedCss(
+      css`
+        ${defaults}
+      `
+    )
+  })
+})
+
+it('variants for components should not be produced in a file without a components layer', () => {
+  let config = {
+    content: [{ raw: html`<div class="sm:container sm:underline"></div>` }],
+  }
+
+  return run('@tailwind utilities', config).then((result) => {
+    return expect(result.css).toMatchFormattedCss(css`
+      @media (min-width: 640px) {
+        .sm\:underline {
+          text-decoration-line: underline;
+        }
+      }
+    `)
+  })
+})
+
+it('variants for utilities should not be produced in a file without a utilities layer', () => {
+  let config = {
+    content: [{ raw: html`<div class="sm:container sm:underline"></div>` }],
+  }
+
+  return run('@tailwind components', config).then((result) => {
+    return expect(result.css).toMatchFormattedCss(css`
+      @media (min-width: 640px) {
+        .sm\:container {
+          width: 100%;
+        }
+        @media (min-width: 640px) {
+          .sm\:container {
+            max-width: 640px;
+          }
+        }
+        @media (min-width: 768px) {
+          .sm\:container {
+            max-width: 768px;
+          }
+        }
+        @media (min-width: 1024px) {
+          .sm\:container {
+            max-width: 1024px;
+          }
+        }
+        @media (min-width: 1280px) {
+          .sm\:container {
+            max-width: 1280px;
+          }
+        }
+        @media (min-width: 1536px) {
+          .sm\:container {
+            max-width: 1536px;
+          }
+        }
+      }
+    `)
+  })
+})