From 2068c5c20830901c8e5fa890730702f79f4a82a5 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Wed, 2 Sep 2020 13:56:51 -0400 Subject: [PATCH 1/3] Purge `layers` by default, deprecate `conservative` mode --- __tests__/purgeUnusedStyles.test.js | 841 +++++++++++++++++----------- src/featureFlags.js | 2 +- src/lib/purgeUnusedStyles.js | 39 +- 3 files changed, 560 insertions(+), 322 deletions(-) diff --git a/__tests__/purgeUnusedStyles.test.js b/__tests__/purgeUnusedStyles.test.js index b9086faf28fa..63a694dcc528 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,275 @@ 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('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 +373,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: { + 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) + }) + }) +) + +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) + }) }) + ) }) diff --git a/src/featureFlags.js b/src/featureFlags.js index aee64d82e5d3..7df28441564d 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 7ba4a2a89d3d..752d27307805 100644 --- a/src/lib/purgeUnusedStyles.js +++ b/src/lib/purgeUnusedStyles.js @@ -3,6 +3,7 @@ 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()) @@ -46,25 +47,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({ From 5821b28e94b7211ed33cdac2b76e868e9d7ffbd4 Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Wed, 2 Sep 2020 14:22:14 -0400 Subject: [PATCH 2/3] Ensure base styles are wrapped in @layer --- __tests__/purgeUnusedStyles.test.js | 62 +++++++++++++++++++++++++++++ src/lib/purgeUnusedStyles.js | 2 + src/util/processPlugins.js | 2 +- 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/__tests__/purgeUnusedStyles.test.js b/__tests__/purgeUnusedStyles.test.js index 63a694dcc528..6fa108d14b8c 100644 --- a/__tests__/purgeUnusedStyles.test.js +++ b/__tests__/purgeUnusedStyles.test.js @@ -325,6 +325,68 @@ test('components are purged by default in layers mode', () => { ) }) +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(() => { diff --git a/src/lib/purgeUnusedStyles.js b/src/lib/purgeUnusedStyles.js index 752d27307805..4422f6cc1a2e 100644 --- a/src/lib/purgeUnusedStyles.js +++ b/src/lib/purgeUnusedStyles.js @@ -9,6 +9,8 @@ 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': diff --git a/src/util/processPlugins.js b/src/util/processPlugins.js index e6effae3b36c..066f5764826b 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) From 660544e70b238e7d3c451ae6941d324bef67b4db Mon Sep 17 00:00:00 2001 From: Adam Wathan Date: Wed, 2 Sep 2020 14:36:53 -0400 Subject: [PATCH 3/3] Update processPlugins test --- __tests__/processPlugins.test.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/__tests__/processPlugins.test.js b/__tests__/processPlugins.test.js index da369bde8122..2afdf9fdb25b 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 + } } `) })