Skip to content

Commit

Permalink
Merge pull request #1639 from tailwindcss/purgecss
Browse files Browse the repository at this point in the history
Integrate PurgeCSS directly into Tailwind
  • Loading branch information
adamwathan authored Apr 28, 2020
2 parents 857e2d3 + 64b6c95 commit da5eaed
Show file tree
Hide file tree
Showing 10 changed files with 468 additions and 23 deletions.
22 changes: 22 additions & 0 deletions __tests__/fixtures/purge-example.html
Original file line number Diff line number Diff line change
@@ -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!
274 changes: 274 additions & 0 deletions __tests__/purgeUnusedStyles.test.js
Original file line number Diff line number Diff line change
@@ -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')
})
})
20 changes: 20 additions & 0 deletions __tests__/responsiveAtRule.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ test('it can generate responsive variants', () => {
.banana { color: yellow; }
.chocolate { color: brown; }
}
@tailwind screens;
`

const output = `
Expand Down Expand Up @@ -52,6 +54,8 @@ test('it can generate responsive variants with a custom separator', () => {
.banana { color: yellow; }
.chocolate { color: brown; }
}
@tailwind screens;
`

const output = `
Expand Down Expand Up @@ -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 = `
Expand Down Expand Up @@ -137,6 +143,8 @@ test('responsive variants are grouped', () => {
@responsive {
.chocolate { color: brown; }
}
@tailwind screens;
`

const output = `
Expand Down Expand Up @@ -181,6 +189,8 @@ test('it can generate responsive variants for nested at-rules', () => {
.grid\\:banana { color: blue; }
}
}
@tailwind screens;
`

const output = `
Expand Down Expand Up @@ -244,6 +254,8 @@ test('it can generate responsive variants for deeply nested at-rules', () => {
}
}
}
@tailwind screens;
`

const output = `
Expand Down Expand Up @@ -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 = `
Expand Down Expand Up @@ -342,6 +356,8 @@ test('responsive variants are generated for all selectors in a rule', () => {
@responsive {
.foo, .bar { color: yellow; }
}
@tailwind screens;
`

const output = `
Expand Down Expand Up @@ -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, {
Expand All @@ -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, {
Expand Down
Loading

0 comments on commit da5eaed

Please sign in to comment.