Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate PurgeCSS directly into Tailwind #1639

Merged
merged 4 commits into from Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions __tests__/fixtures/purge-example.html
@@ -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
@@ -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
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