Merge pull request #1639 from tailwindcss/purgecss

Integrate PurgeCSS directly into Tailwind
This commit is contained in:
Adam Wathan 2020-04-28 14:17:05 -04:00 committed by GitHub
commit da5eaed1c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 468 additions and 23 deletions

View File

@ -0,0 +1,22 @@
<!-- Basic HTML -->
<div class="bg-red-500 md:bg-blue-300 w-1/2"></div>
<!-- Vue dynamic classes -->
<span :class="{ block: enabled, 'md:flow-root': !enabled }"></span>
<!-- JSX with template strings -->
<script>
function Component() {
return <div class={`h-screen`}></div>
}
</script>
<!-- Custom classes with really weird characters -->
<div class="min-h-(screen-4) bg-black! font-%#$@ w-(1/2+8)"></div>
<!-- Pug -->
span.inline-grid.grid-cols-3(class="px-1.5")
.col-span-2
Hello
.col-span-1.text-center
World!

View File

@ -0,0 +1,274 @@
import fs from 'fs'
import path from 'path'
import postcss from 'postcss'
import tailwind from '../src/index'
import defaultConfig from '../stubs/defaultConfig.stub.js'
const config = {
...defaultConfig,
theme: {
extend: {
colors: {
'black!': '#000',
},
spacing: {
'1.5': '0.375rem',
'(1/2+8)': 'calc(50% + 2rem)',
},
minHeight: {
'(screen-4)': 'calc(100vh - 1rem)',
},
fontFamily: {
'%#$@': 'Comic Sans',
},
},
},
}
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
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('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('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
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('purgecss options can be provided', () => {
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).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('.md\\:bg-green-500')
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('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).not.toContain('html')
expect(result.css).not.toContain('body')
expect(result.css).not.toContain('button')
expect(result.css).not.toContain('legend')
expect(result.css).not.toContain('progress')
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('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')
})
})

View File

@ -12,6 +12,8 @@ test('it can generate responsive variants', () => {
.banana { color: yellow; }
.chocolate { color: brown; }
}
@tailwind screens;
`
const output = `
@ -52,6 +54,8 @@ test('it can generate responsive variants with a custom separator', () => {
.banana { color: yellow; }
.chocolate { color: brown; }
}
@tailwind screens;
`
const output = `
@ -92,6 +96,8 @@ test('it can generate responsive variants when classes have non-standard charact
.hover\\:banana { color: yellow; }
.chocolate-2\\.5 { color: brown; }
}
@tailwind screens;
`
const output = `
@ -137,6 +143,8 @@ test('responsive variants are grouped', () => {
@responsive {
.chocolate { color: brown; }
}
@tailwind screens;
`
const output = `
@ -181,6 +189,8 @@ test('it can generate responsive variants for nested at-rules', () => {
.grid\\:banana { color: blue; }
}
}
@tailwind screens;
`
const output = `
@ -244,6 +254,8 @@ test('it can generate responsive variants for deeply nested at-rules', () => {
}
}
}
@tailwind screens;
`
const output = `
@ -307,6 +319,8 @@ test('screen prefix is only applied to the last class in a selector', () => {
@responsive {
.banana li * .sandwich #foo > div { color: yellow; }
}
@tailwind screens;
`
const output = `
@ -342,6 +356,8 @@ test('responsive variants are generated for all selectors in a rule', () => {
@responsive {
.foo, .bar { color: yellow; }
}
@tailwind screens;
`
const output = `
@ -377,6 +393,8 @@ test('selectors with no classes cannot be made responsive', () => {
@responsive {
div { color: yellow; }
}
@tailwind screens;
`
expect.assertions(1)
return run(input, {
@ -398,6 +416,8 @@ test('all selectors in a rule must contain classes', () => {
@responsive {
.foo, div { color: yellow; }
}
@tailwind screens;
`
expect.assertions(1)
return run(input, {

View File

@ -77,3 +77,19 @@ it('generates the right CSS with implicit screen utilities', () => {
expect(result.css).toBe(expected)
})
})
it('generates the right CSS when "important" is enabled', () => {
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([tailwind({ ...config, important: true })])
.process(input, { from: inputPath })
.then(result => {
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output-important.css`),
'utf8'
)
expect(result.css).toBe(expected)
})
})

View File

@ -41,6 +41,7 @@
"rimraf": "^3.0.0"
},
"dependencies": {
"@fullhuman/postcss-purgecss": "^2.1.2",
"autoprefixer": "^9.4.5",
"bytes": "^3.0.0",
"chalk": "^4.0.0",

View File

@ -0,0 +1,73 @@
import _ from 'lodash'
import postcss from 'postcss'
import purgecss from '@fullhuman/postcss-purgecss'
function removeTailwindComments(css) {
css.walkComments(comment => {
switch (comment.text.trim()) {
case 'tailwind start components':
case 'tailwind start utilities':
case 'tailwind start screens':
case 'tailwind end components':
case 'tailwind end utilities':
case 'tailwind end screens':
comment.remove()
break
default:
break
}
})
}
export default function purgeUnusedUtilities(config) {
const purgeEnabled =
_.get(config, 'purge.enabled', false) === true ||
(config.purge !== undefined && process.env.NODE_ENV === 'production')
if (!purgeEnabled) {
return removeTailwindComments
}
return postcss([
function(css) {
const mode = _.get(config, 'purge.mode', 'conservative')
if (mode === 'conservative') {
css.prepend(postcss.comment({ text: 'purgecss start ignore' }))
css.append(postcss.comment({ text: 'purgecss end ignore' }))
css.walkComments(comment => {
switch (comment.text.trim()) {
case 'tailwind start components':
case 'tailwind start utilities':
case 'tailwind start screens':
comment.text = 'purgecss end ignore'
break
case 'tailwind end components':
case 'tailwind end utilities':
case 'tailwind end screens':
comment.text = 'purgecss start ignore'
break
default:
break
}
})
} else if (mode === 'all') {
removeTailwindComments(css)
}
},
purgecss({
content: Array.isArray(config.purge) ? config.purge : config.purge.content,
defaultExtractor: content => {
// Capture as liberally as possible, including things like `h-(screen-1.5)`
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []
// Capture classes within other delimiters like .block(class="w-1/2") in Pug
const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || []
return broadMatches.concat(innerMatches)
},
...config.purge.options,
}),
])
}

View File

@ -43,22 +43,16 @@ export default function(config) {
const hasScreenRules = finalRules.some(i => i.nodes.length !== 0)
if (!hasScreenRules) {
return
}
let includesScreensExplicitly = false
css.walkAtRules('tailwind', atRule => {
if (atRule.params === 'screens') {
atRule.replaceWith(finalRules)
includesScreensExplicitly = true
if (atRule.params !== 'screens') {
return
}
})
if (!includesScreensExplicitly) {
css.append(finalRules)
return
}
if (hasScreenRules) {
atRule.before(finalRules)
}
atRule.remove()
})
}
}

View File

@ -40,6 +40,8 @@ export default function(
}
})
let includesScreensExplicitly = false
css.walkAtRules('tailwind', atRule => {
if (atRule.params === 'preflight') {
// prettier-ignore
@ -52,14 +54,32 @@ export default function(
}
if (atRule.params === 'components') {
atRule.before(postcss.comment({ text: 'tailwind start components' }))
atRule.before(updateSource(pluginComponents, atRule.source))
atRule.after(postcss.comment({ text: 'tailwind end components' }))
atRule.remove()
}
if (atRule.params === 'utilities') {
atRule.before(postcss.comment({ text: 'tailwind start utilities' }))
atRule.before(updateSource(pluginUtilities, atRule.source))
atRule.after(postcss.comment({ text: 'tailwind end utilities' }))
atRule.remove()
}
if (atRule.params === 'screens') {
includesScreensExplicitly = true
atRule.before(postcss.comment({ text: 'tailwind start screens' }))
atRule.after(postcss.comment({ text: 'tailwind end screens' }))
}
})
if (!includesScreensExplicitly) {
css.append([
postcss.comment({ text: 'tailwind start screens' }),
postcss.atRule({ name: 'tailwind', params: 'screens' }),
postcss.comment({ text: 'tailwind end screens' }),
])
}
}
}

View File

@ -7,6 +7,7 @@ import substituteVariantsAtRules from './lib/substituteVariantsAtRules'
import substituteResponsiveAtRules from './lib/substituteResponsiveAtRules'
import substituteScreenAtRules from './lib/substituteScreenAtRules'
import substituteClassApplyAtRules from './lib/substituteClassApplyAtRules'
import purgeUnusedStyles from './lib/purgeUnusedStyles'
import corePlugins from './corePlugins'
import processPlugins from './util/processPlugins'
@ -23,6 +24,7 @@ export default function(getConfig) {
substituteResponsiveAtRules(config),
substituteScreenAtRules(config),
substituteClassApplyAtRules(config, processedPlugins.utilities),
purgeUnusedStyles(config),
]).process(css, { from: _.get(css, 'source.input.file') })
}
}

View File

@ -845,6 +845,14 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@fullhuman/postcss-purgecss@^2.1.2":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-2.1.2.tgz#8fe4d4ae2b58214b5452cb490a31c7146517442f"
integrity sha512-Jf34YVBK9GtXTblpu0svNUJdA7rTQoRMz+yEJe6mwTnXDIGipWLzaX/VgU/x6IPC6WvU5SY/XlawwqhxoyFPTg==
dependencies:
postcss "7.0.27"
purgecss "^2.1.2"
"@istanbuljs/load-nyc-config@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz#10602de5570baea82f8afbfa2630b24e7a8cfe5b"
@ -1715,6 +1723,11 @@ commander@^4.0.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-4.0.1.tgz#b67622721785993182e807f4883633e6401ba53c"
integrity sha512-IPF4ouhCP+qdlcmCedhxX4xiGBPyigb8v5NeUp+0LyhwLgxMqyp3S0vl7TAPfS/hiP7FC3caI/PB9lTmP8r1NA==
commander@^5.0.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@ -4266,6 +4279,15 @@ postcss-value-parser@^4.0.3:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d"
integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg==
postcss@7.0.27, postcss@^7.0.11, postcss@^7.0.18, postcss@^7.0.21, postcss@^7.0.27:
version "7.0.27"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
supports-color "^6.1.0"
postcss@^6.0.9:
version "6.0.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
@ -4275,15 +4297,6 @@ postcss@^6.0.9:
source-map "^0.6.1"
supports-color "^5.4.0"
postcss@^7.0.11, postcss@^7.0.18, postcss@^7.0.21, postcss@^7.0.27:
version "7.0.27"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9"
integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
supports-color "^6.1.0"
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@ -4362,6 +4375,16 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
purgecss@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-2.1.2.tgz#96f666d04c56705208aaa1a544b5f22e13828955"
integrity sha512-5oDBxiT9VonwKmEMohPFRFZrj8fdSVKxHPwq7G5Rx/2pXicZFJu+D4m5bb3NuV0sSK3ooNxq5jFIwwHzifP5FA==
dependencies:
commander "^5.0.0"
glob "^7.0.0"
postcss "7.0.27"
postcss-selector-parser "^6.0.2"
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"