Repository: tailwindlabs/tailwindcss
Pull request #2288 (2020-09-02 18:32:02)

Purge `layers` by default, deprecate `conservative` mode
This PR introduces a new `layers` mode to the PurgeCSS configuration, and makes it the default, deprecating the existing `conservative` mode.

When configured manually, it looks like this:

```js
// tailwind.config.js
module.exports = {
  purge: {
    mode: 'layers',
    layers: ['base', 'components', 'utilities'],
    content: [
      // Paths...
    ]
  }
}
```

It allows you to tell Tailwind which _layers_ it should purge (base, components, and/or utilities). The old `conservative` mode was the equivalent of this:

```js
// tailwind.config.js
module.exports = {
  purge: {
    mode: 'layers',
    layers: ['utilities'],
    content: [
      // Paths...
    ]
  }
}
```

Now that we've added the `preserveHtmlElements` option and default it to `true`, I feel like it's safe to purge _all_ layers by default.

It's important to note that this will only purge rules that are explicitly stored in a `layer`, which is rules that are added by a plugin, or rules that are added via custom CSS but explicitly wrapped in a `@layer` at-rule:

```css
/* Won't be purged, not in a layer */
.foo {
  color: blue
}

/* Will be purged, explicitly given a layer */
@layer utilities {
  .bar {
    color: red
  }
}
```

This PR makes `@layer` a real public API, whereas before it was sort of private.

You can still use `mode: 'all'` to purge _all_ of your CSS, including custom CSS and any third party CSS:

```js
// tailwind.config.js
module.exports = {
  purge: {
    mode: 'all',
    content: [
      // Paths...
    ]
  }
}
```

Overall, the goal of this PR is to standardize on some fundamental terminology and core concepts, as well as to purge more aggressively now that it's safe to do thanks to `preserveHtmlElements`.

Purging using the new `layers` mode by default is behind a `future` flag called `purgeLayersByDefault`. Without enabling that flag, the mode will be `conservative` by default, and the user will see a warning that that mode has been deprecated.

```js
// tailwind.config.js
module.exports = {
  future: {
    purgeLayersByDefault: true
  },
  // ...
}
```

One other thing worth noting (that will affect nobody really) is that prior to this PR, any custom CSS wrapped in a `@responsive` at-rule was automatically considered part of the `utilities` layer. This is no longer the case — anything without an explicit layer simply has no layer and won't be considered for purging without setting `mode` to `all`. The only side effect of this is that you might have a slightly larger production CSS file by default now, because your custom utilities that used `@responsive` were being purged by the "conservative" mode before and are not being purged by the "conservative" mode now.

You can fix this by just wrapping any custom utilities in `@layer utilities { ... }`:

```css
/* Before this was automatically in the "utilities" layer */
@responsive {
  .foo {
    color: blue
  }
}

/* Now need to be explicit */
@layer utilities {
  @responsive {
    .foo {
      color: blue
    }
  }
}
```


Patch:
diff --git a/package.json b/package.json
index 91d72c78..2e81ec22 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
   "license": "MIT",
   "main": "lib/index.js",
   "style": "dist/tailwind.css",
-  "repository": "https://github.com/tailwindcss/tailwindcss.git",
-  "bugs": "https://github.com/tailwindcss/tailwindcss/issues",
+  "repository": "https://github.com/tailwindlabs/tailwindcss.git",
+  "bugs": "https://github.com/tailwindlabs/tailwindcss/issues",
   "homepage": "https://tailwindcss.com",
   "bin": {
     "tailwind": "lib/cli.js",
diff --git a/src/featureFlags.js b/src/featureFlags.js
index aee64d82..7df28441 100644
--- a/src/featureFlags.js
+++ b/src/featureFlags.js
@@ -3,7 +3,7 @@ import chalk from 'chalk'
 import log from './util/log'
 
 const featureFlags = {
-  future: ['removeDeprecatedGapUtilities'],
+  future: ['removeDeprecatedGapUtilities', 'purgeLayersByDefault'],
   experimental: [
     'uniformColorPalette',
     'extendedSpacingScale',
diff --git a/src/lib/purgeUnusedStyles.js b/src/lib/purgeUnusedStyles.js
index 7ba4a2a8..4422f6cc 100644
--- a/src/lib/purgeUnusedStyles.js
+++ b/src/lib/purgeUnusedStyles.js
@@ -3,11 +3,14 @@ import postcss from 'postcss'
 import purgecss from '@fullhuman/postcss-purgecss'
 import log from '../util/log'
 import htmlTags from 'html-tags'
+import { flagEnabled } from '../featureFlags'
 
 function removeTailwindMarkers(css) {
   css.walkAtRules('tailwind', rule => rule.remove())
   css.walkComments(comment => {
     switch (comment.text.trim()) {
+      case 'tailwind start base':
+      case 'tailwind end base':
       case 'tailwind start components':
       case 'tailwind start utilities':
       case 'tailwind end components':
@@ -46,25 +49,49 @@ export default function purgeUnusedUtilities(config, configChanged) {
 
   return postcss([
     function(css) {
-      const mode = _.get(config, 'purge.mode', 'conservative')
+      const mode = _.get(
+        config,
+        'purge.mode',
+        flagEnabled(config, 'purgeLayersByDefault') ? 'layers' : 'conservative'
+      )
+
+      if (!['all', 'layers', 'conservative'].includes(mode)) {
+        throw new Error('Purge `mode` must be one of `layers` or `all`.')
+      }
+
+      if (mode === 'all') {
+        return
+      }
 
       if (mode === 'conservative') {
-        css.prepend(postcss.comment({ text: 'purgecss start ignore' }))
-        css.append(postcss.comment({ text: 'purgecss end ignore' }))
+        log.warn([
+          'The `conservative` purge mode will be removed in Tailwind 2.0.',
+          'Please switch to the new `layers` mode instead.',
+        ])
+      }
+
+      const layers =
+        mode === 'conservative'
+          ? ['utilities']
+          : _.get(config, 'purge.layers', ['base', 'components', 'utilities'])
 
-        css.walkComments(comment => {
+      css.prepend(postcss.comment({ text: 'purgecss start ignore' }))
+      css.append(postcss.comment({ text: 'purgecss end ignore' }))
+
+      css.walkComments(comment => {
+        layers.forEach(layer => {
           switch (comment.text.trim()) {
-            case 'tailwind start utilities':
+            case `tailwind start ${layer}`:
               comment.text = 'purgecss end ignore'
               break
-            case 'tailwind end utilities':
+            case `tailwind end ${layer}`:
               comment.text = 'purgecss start ignore'
               break
             default:
               break
           }
         })
-      }
+      })
     },
     removeTailwindMarkers,
     purgecss({
diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js
index e6effae3..066f5764 100644
--- a/src/util/processPlugins.js
+++ b/src/util/processPlugins.js
@@ -124,7 +124,7 @@ export default function(plugins, config) {
         )
       },
       addBase: baseStyles => {
-        pluginBaseStyles.push(...parseStyles(baseStyles))
+        pluginBaseStyles.push(wrapWithLayer(parseStyles(baseStyles), 'base'))
       },
       addVariant: (name, generator) => {
         pluginVariantGenerators[name] = generateVariantFunction(generator)


Test patch:
diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js
index da369bde..2afdf9fd 100644
--- a/__tests__/processPlugins.test.js
+++ b/__tests__/processPlugins.test.js
@@ -288,11 +288,13 @@ test('plugins can add base styles with object syntax', () => {
   )
 
   expect(css(base)).toMatchCss(`
-    img {
-      max-width: 100%
-    }
-    button {
-      font-family: inherit
+    @layer base {
+      img {
+        max-width: 100%
+      }
+      button {
+        font-family: inherit
+      }
     }
   `)
 })
@@ -321,11 +323,13 @@ test('plugins can add base styles with raw PostCSS nodes', () => {
   )
 
   expect(css(base)).toMatchCss(`
-    img {
-      max-width: 100%
-    }
-    button {
-      font-family: inherit
+    @layer base {
+      img {
+        max-width: 100%
+      }
+      button {
+        font-family: inherit
+      }
     }
   `)
 })
diff --git a/__tests__/purgeUnusedStyles.test.js b/__tests__/purgeUnusedStyles.test.js
index b9086faf..6fa108d1 100644
--- a/__tests__/purgeUnusedStyles.test.js
+++ b/__tests__/purgeUnusedStyles.test.js
@@ -28,6 +28,15 @@ function extractRules(root) {
   return rules
 }
 
+async function inProduction(callback) {
+  const OLD_NODE_ENV = process.env.NODE_ENV
+  process.env.NODE_ENV = 'production'
+
+  const result = await callback()
+  process.env.NODE_ENV = OLD_NODE_ENV
+  return result
+}
+
 const config = {
   ...defaultConfig,
   theme: {
@@ -85,108 +94,337 @@ function assertPurged(result) {
 }
 
 test('purges unused classes', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'production'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...config,
-      purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-
-      assertPurged(result)
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          assertPurged(result)
+        })
+    })
+  )
+})
+
+test('custom css is not purged by default', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      return postcss([
+        tailwind({
+          ...config,
+          purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+        }),
+      ])
+        .process(
+          `
+        @tailwind base;
+
+        @tailwind components;
+
+        @tailwind utilities;
+
+        .example {
+          @apply font-bold;
+          color: theme('colors.red.500');
+        }
+      `,
+          { from: null }
+        )
+        .then(result => {
+          const rules = extractRules(result.root)
+          assertPurged(result)
+          expect(rules).toContain('.example')
+        })
+    })
+  )
+})
+
+test('custom css that uses @responsive is not purged by default', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      return postcss([
+        tailwind({
+          ...config,
+          purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+        }),
+      ])
+        .process(
+          `
+        @tailwind base;
+
+        @tailwind components;
+
+        @tailwind utilities;
+
+        @responsive {
+          .example {
+            @apply font-bold;
+            color: theme('colors.red.500');
+          }
+        }
+      `,
+          { from: null }
+        )
+        .then(result => {
+          const rules = extractRules(result.root)
+          assertPurged(result)
+          expect(rules).toContain('.example')
+        })
+    })
+  )
+})
+
+test('custom css in a layer is purged by default when using layers mode', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      return postcss([
+        tailwind({
+          ...config,
+          future: {
+            purgeLayersByDefault: true,
+          },
+          purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+        }),
+      ])
+        .process(
+          `
+        @tailwind base;
+
+        @tailwind components;
+
+        @layer components {
+          .example {
+            @apply font-bold;
+            color: theme('colors.red.500');
+          }
+        }
+
+        @tailwind utilities;
+      `,
+          { from: null }
+        )
+        .then(result => {
+          const rules = extractRules(result.root)
+          assertPurged(result)
+          expect(rules).not.toContain('.example')
+        })
+    })
+  )
+})
+
+test('custom css in a layer in a @responsive at-rule is purged by default', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      return postcss([
+        tailwind({
+          ...config,
+          future: {
+            purgeLayersByDefault: true,
+          },
+          purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+        }),
+      ])
+        .process(
+          `
+        @tailwind base;
+
+        @tailwind components;
+
+        @layer components {
+          @responsive {
+            .example {
+              @apply font-bold;
+              color: theme('colors.red.500');
+            }
+          }
+        }
+
+        @tailwind utilities;
+      `,
+          { from: null }
+        )
+        .then(result => {
+          const rules = extractRules(result.root)
+          assertPurged(result)
+          expect(rules).not.toContain('.example')
+        })
     })
+  )
 })
 
 test('purges unused classes with important string', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'production'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...config,
-      important: '#tailwind',
-      purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-
-      assertPurged(result)
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          important: '#tailwind',
+          purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          assertPurged(result)
+        })
     })
+  )
 })
 
-test('does not purge components', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'production'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...config,
-      purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-
-      expect(result.css).toContain('.container')
-      assertPurged(result)
+test('mode must be a valid value', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return expect(
+        postcss([
+          tailwind({
+            ...config,
+            purge: {
+              mode: 'poop',
+              content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+            },
+          }),
+        ]).process(input, { from: inputPath })
+      ).rejects.toThrow()
     })
+  )
 })
 
-test('does not purge except in production', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'development'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...defaultConfig,
-      purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-      const expected = fs.readFileSync(
-        path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
-        'utf8'
-      )
-
-      expect(result.css).toBe(expected)
+test('components are purged by default in layers mode', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          future: {
+            purgeLayersByDefault: true,
+          },
+          purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          expect(result.css).not.toContain('.container')
+          assertPurged(result)
+        })
+    })
+  )
+})
+
+test('you can specify which layers to purge', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          future: {
+            purgeLayersByDefault: true,
+          },
+          purge: {
+            mode: 'layers',
+            layers: ['utilities'],
+            content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+          },
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          const rules = extractRules(result.root)
+          expect(rules).toContain('optgroup')
+          expect(rules).toContain('.container')
+          assertPurged(result)
+        })
+    })
+  )
+})
+
+test('you can purge just base and component layers (but why)', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          future: {
+            purgeLayersByDefault: true,
+          },
+          purge: {
+            mode: 'layers',
+            layers: ['base', 'components'],
+            content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+          },
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          const rules = extractRules(result.root)
+          expect(rules).not.toContain('[type="checkbox"]')
+          expect(rules).not.toContain('.container')
+          expect(rules).toContain('.float-left')
+          expect(rules).toContain('.md\\:bg-red-500')
+          expect(rules).toContain('.lg\\:appearance-none')
+        })
     })
+  )
+})
+
+test('does not purge components when mode is conservative', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          purge: {
+            mode: 'conservative',
+            content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+          },
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          expect(result.css).toContain('.container')
+          assertPurged(result)
+        })
+    })
+  )
 })
 
 test(
-  'does not purge if the array is empty',
+  'does not purge except in production',
   suppressConsoleLogs(() => {
-    const OLD_NODE_ENV = process.env.NODE_ENV
-    process.env.NODE_ENV = 'production'
     const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
     const input = fs.readFileSync(inputPath, 'utf8')
 
     return postcss([
       tailwind({
         ...defaultConfig,
-        purge: [],
+        purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
       }),
     ])
       .process(input, { from: inputPath })
       .then(result => {
-        process.env.NODE_ENV = OLD_NODE_ENV
         const expected = fs.readFileSync(
           path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
           'utf8'
@@ -197,269 +435,306 @@ test(
   })
 )
 
+test('does not purge if the array is empty', () => {
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const OLD_NODE_ENV = process.env.NODE_ENV
+      process.env.NODE_ENV = 'production'
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...defaultConfig,
+          purge: [],
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          process.env.NODE_ENV = OLD_NODE_ENV
+          const expected = fs.readFileSync(
+            path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
+            'utf8'
+          )
+
+          expect(result.css).toBe(expected)
+        })
+    })
+  )
+})
+
 test('does not purge if explicitly disabled', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'production'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...defaultConfig,
-      purge: { enabled: false },
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-      const expected = fs.readFileSync(
-        path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
-        'utf8'
-      )
-
-      expect(result.css).toBe(expected)
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...defaultConfig,
+          purge: { enabled: false },
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          const expected = fs.readFileSync(
+            path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
+            'utf8'
+          )
+
+          expect(result.css).toBe(expected)
+        })
     })
+  )
 })
 
 test('does not purge if purge is simply false', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'production'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...defaultConfig,
-      purge: false,
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-      const expected = fs.readFileSync(
-        path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
-        'utf8'
-      )
-
-      expect(result.css).toBe(expected)
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...defaultConfig,
+          purge: false,
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          const expected = fs.readFileSync(
+            path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
+            'utf8'
+          )
+
+          expect(result.css).toBe(expected)
+        })
     })
+  )
 })
 
 test('purges outside of production if explicitly enabled', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'development'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...config,
-      purge: { enabled: true, content: [path.resolve(`${__dirname}/fixtures/**/*.html`)] },
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-
-      assertPurged(result)
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          purge: { enabled: true, content: [path.resolve(`${__dirname}/fixtures/**/*.html`)] },
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          assertPurged(result)
+        })
     })
+  )
 })
 
-test('purgecss options can be provided', () => {
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
+test(
+  'purgecss options can be provided',
+  suppressConsoleLogs(() => {
+    const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+    const input = fs.readFileSync(inputPath, 'utf8')
+
+    return postcss([
+      tailwind({
+        ...config,
+        purge: {
+          enabled: true,
+          options: {
+            content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+            whitelist: ['md:bg-green-500'],
+          },
+        },
+      }),
+    ])
+      .process(input, { from: inputPath })
+      .then(result => {
+        expect(result.css).toContain('.md\\:bg-green-500')
+        assertPurged(result)
+      })
+  })
+)
 
-  return postcss([
-    tailwind({
-      ...config,
-      purge: {
-        enabled: true,
-        options: {
+test(
+  'can purge all CSS, not just Tailwind classes',
+  suppressConsoleLogs(() => {
+    const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+    const input = fs.readFileSync(inputPath, 'utf8')
+
+    return postcss([
+      tailwind({
+        ...config,
+        purge: {
+          enabled: true,
+          mode: 'all',
           content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
-          whitelist: ['md:bg-green-500'],
         },
+      }),
+      function(css) {
+        // Remove any comments to avoid accidentally asserting against them
+        // instead of against real CSS rules.
+        css.walkComments(c => c.remove())
       },
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      expect(result.css).toContain('.md\\:bg-green-500')
-      assertPurged(result)
-    })
-})
+    ])
+      .process(input, { from: inputPath })
+      .then(result => {
+        expect(result.css).toContain('html')
+        expect(result.css).toContain('body')
+        expect(result.css).toContain('samp')
+        expect(result.css).not.toContain('.example')
+        expect(result.css).not.toContain('.sm\\:example')
 
-test('can purge all CSS, not just Tailwind classes', () => {
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...config,
-      purge: {
-        enabled: true,
-        mode: 'all',
-        content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
-      },
-    }),
-    function(css) {
-      // Remove any comments to avoid accidentally asserting against them
-      // instead of against real CSS rules.
-      css.walkComments(c => c.remove())
-    },
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      expect(result.css).toContain('html')
-      expect(result.css).toContain('body')
-      expect(result.css).toContain('samp')
-      expect(result.css).not.toContain('.example')
-      expect(result.css).not.toContain('.sm\\:example')
-
-      assertPurged(result)
-    })
-})
+        assertPurged(result)
+      })
+  })
+)
 
 test('the `conservative` mode can be set explicitly', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'production'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...config,
-      purge: {
-        mode: 'conservative',
-        content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
-      },
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-
-      expect(result.css).not.toContain('.bg-red-600')
-      expect(result.css).not.toContain('.w-1\\/3')
-      expect(result.css).not.toContain('.flex')
-      expect(result.css).not.toContain('.font-sans')
-      expect(result.css).not.toContain('.text-right')
-      expect(result.css).not.toContain('.px-4')
-      expect(result.css).not.toContain('.h-full')
-
-      expect(result.css).toContain('.bg-red-500')
-      expect(result.css).toContain('.md\\:bg-blue-300')
-      expect(result.css).toContain('.w-1\\/2')
-      expect(result.css).toContain('.block')
-      expect(result.css).toContain('.md\\:flow-root')
-      expect(result.css).toContain('.h-screen')
-      expect(result.css).toContain('.min-h-\\(screen-4\\)')
-      expect(result.css).toContain('.bg-black\\!')
-      expect(result.css).toContain('.font-\\%\\#\\$\\@')
-      expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
-      expect(result.css).toContain('.inline-grid')
-      expect(result.css).toContain('.grid-cols-3')
-      expect(result.css).toContain('.px-1\\.5')
-      expect(result.css).toContain('.col-span-2')
-      expect(result.css).toContain('.col-span-1')
-      expect(result.css).toContain('.text-center')
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          purge: {
+            mode: 'conservative',
+            content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+          },
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          expect(result.css).not.toContain('.bg-red-600')
+          expect(result.css).not.toContain('.w-1\\/3')
+          expect(result.css).not.toContain('.flex')
+          expect(result.css).not.toContain('.font-sans')
+          expect(result.css).not.toContain('.text-right')
+          expect(result.css).not.toContain('.px-4')
+          expect(result.css).not.toContain('.h-full')
+
+          expect(result.css).toContain('.bg-red-500')
+          expect(result.css).toContain('.md\\:bg-blue-300')
+          expect(result.css).toContain('.w-1\\/2')
+          expect(result.css).toContain('.block')
+          expect(result.css).toContain('.md\\:flow-root')
+          expect(result.css).toContain('.h-screen')
+          expect(result.css).toContain('.min-h-\\(screen-4\\)')
+          expect(result.css).toContain('.bg-black\\!')
+          expect(result.css).toContain('.font-\\%\\#\\$\\@')
+          expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
+          expect(result.css).toContain('.inline-grid')
+          expect(result.css).toContain('.grid-cols-3')
+          expect(result.css).toContain('.px-1\\.5')
+          expect(result.css).toContain('.col-span-2')
+          expect(result.css).toContain('.col-span-1')
+          expect(result.css).toContain('.text-center')
+        })
     })
+  )
 })
 
 test('element selectors are preserved by default', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'production'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...config,
-      purge: {
-        content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
-        mode: 'all',
-      },
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-      const rules = extractRules(result.root)
-      ;[
-        'a',
-        'blockquote',
-        'body',
-        'code',
-        'fieldset',
-        'figure',
-        'h1',
-        'h2',
-        'h3',
-        'h4',
-        'h5',
-        'h6',
-        'hr',
-        'html',
-        'img',
-        'kbd',
-        'ol',
-        'p',
-        'pre',
-        'strong',
-        'sup',
-        'table',
-        'ul',
-      ].forEach(e => expect(rules).toContain(e))
-
-      assertPurged(result)
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          purge: {
+            content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+            mode: 'all',
+          },
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          const rules = extractRules(result.root)
+          ;[
+            'a',
+            'blockquote',
+            'body',
+            'code',
+            'fieldset',
+            'figure',
+            'h1',
+            'h2',
+            'h3',
+            'h4',
+            'h5',
+            'h6',
+            'hr',
+            'html',
+            'img',
+            'kbd',
+            'ol',
+            'p',
+            'pre',
+            'strong',
+            'sup',
+            'table',
+            'ul',
+          ].forEach(e => expect(rules).toContain(e))
+
+          assertPurged(result)
+        })
     })
+  )
 })
 
 test('preserving element selectors can be disabled', () => {
-  const OLD_NODE_ENV = process.env.NODE_ENV
-  process.env.NODE_ENV = 'production'
-  const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
-  const input = fs.readFileSync(inputPath, 'utf8')
-
-  return postcss([
-    tailwind({
-      ...config,
-      purge: {
-        content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
-        mode: 'all',
-        preserveHtmlElements: false,
-      },
-    }),
-  ])
-    .process(input, { from: inputPath })
-    .then(result => {
-      process.env.NODE_ENV = OLD_NODE_ENV
-
-      const rules = extractRules(result.root)
-
-      ;[
-        'blockquote',
-        'code',
-        'em',
-        'fieldset',
-        'figure',
-        'h1',
-        'h2',
-        'h3',
-        'h4',
-        'h5',
-        'h6',
-        'hr',
-        'img',
-        'kbd',
-        'li',
-        'ol',
-        'pre',
-        'strong',
-        'sup',
-        'table',
-        'ul',
-      ].forEach(e => expect(rules).not.toContain(e))
-
-      assertPurged(result)
+  return inProduction(
+    suppressConsoleLogs(() => {
+      const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
+      const input = fs.readFileSync(inputPath, 'utf8')
+
+      return postcss([
+        tailwind({
+          ...config,
+          purge: {
+            content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
+            mode: 'all',
+            preserveHtmlElements: false,
+          },
+        }),
+      ])
+        .process(input, { from: inputPath })
+        .then(result => {
+          const rules = extractRules(result.root)
+
+          ;[
+            'blockquote',
+            'code',
+            'em',
+            'fieldset',
+            'figure',
+            'h1',
+            'h2',
+            'h3',
+            'h4',
+            'h5',
+            'h6',
+            'hr',
+            'img',
+            'kbd',
+            'li',
+            'ol',
+            'pre',
+            'strong',
+            'sup',
+            'table',
+            'ul',
+          ].forEach(e => expect(rules).not.toContain(e))
+
+          assertPurged(result)
+        })
     })
+  )
 })