Skip to content

Commit da5eaed

Browse files
authored
Merge pull request #1639 from tailwindcss/purgecss
Integrate PurgeCSS directly into Tailwind
2 parents 857e2d3 + 64b6c95 commit da5eaed

10 files changed

+468
-23
lines changed

__tests__/fixtures/purge-example.html

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<!-- Basic HTML -->
2+
<div class="bg-red-500 md:bg-blue-300 w-1/2"></div>
3+
4+
<!-- Vue dynamic classes -->
5+
<span :class="{ block: enabled, 'md:flow-root': !enabled }"></span>
6+
7+
<!-- JSX with template strings -->
8+
<script>
9+
function Component() {
10+
return <div class={`h-screen`}></div>
11+
}
12+
</script>
13+
14+
<!-- Custom classes with really weird characters -->
15+
<div class="min-h-(screen-4) bg-black! font-%#$@ w-(1/2+8)"></div>
16+
17+
<!-- Pug -->
18+
span.inline-grid.grid-cols-3(class="px-1.5")
19+
.col-span-2
20+
Hello
21+
.col-span-1.text-center
22+
World!

__tests__/purgeUnusedStyles.test.js

+274
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import postcss from 'postcss'
4+
import tailwind from '../src/index'
5+
import defaultConfig from '../stubs/defaultConfig.stub.js'
6+
7+
const config = {
8+
...defaultConfig,
9+
theme: {
10+
extend: {
11+
colors: {
12+
'black!': '#000',
13+
},
14+
spacing: {
15+
'1.5': '0.375rem',
16+
'(1/2+8)': 'calc(50% + 2rem)',
17+
},
18+
minHeight: {
19+
'(screen-4)': 'calc(100vh - 1rem)',
20+
},
21+
fontFamily: {
22+
'%#$@': 'Comic Sans',
23+
},
24+
},
25+
},
26+
}
27+
28+
test('purges unused classes', () => {
29+
const OLD_NODE_ENV = process.env.NODE_ENV
30+
process.env.NODE_ENV = 'production'
31+
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
32+
const input = fs.readFileSync(inputPath, 'utf8')
33+
34+
return postcss([
35+
tailwind({
36+
...config,
37+
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
38+
}),
39+
])
40+
.process(input, { from: inputPath })
41+
.then(result => {
42+
process.env.NODE_ENV = OLD_NODE_ENV
43+
44+
expect(result.css).not.toContain('.bg-red-600')
45+
expect(result.css).not.toContain('.w-1\\/3')
46+
expect(result.css).not.toContain('.flex')
47+
expect(result.css).not.toContain('.font-sans')
48+
expect(result.css).not.toContain('.text-right')
49+
expect(result.css).not.toContain('.px-4')
50+
expect(result.css).not.toContain('.h-full')
51+
52+
expect(result.css).toContain('.bg-red-500')
53+
expect(result.css).toContain('.md\\:bg-blue-300')
54+
expect(result.css).toContain('.w-1\\/2')
55+
expect(result.css).toContain('.block')
56+
expect(result.css).toContain('.md\\:flow-root')
57+
expect(result.css).toContain('.h-screen')
58+
expect(result.css).toContain('.min-h-\\(screen-4\\)')
59+
expect(result.css).toContain('.bg-black\\!')
60+
expect(result.css).toContain('.font-\\%\\#\\$\\@')
61+
expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
62+
expect(result.css).toContain('.inline-grid')
63+
expect(result.css).toContain('.grid-cols-3')
64+
expect(result.css).toContain('.px-1\\.5')
65+
expect(result.css).toContain('.col-span-2')
66+
expect(result.css).toContain('.col-span-1')
67+
expect(result.css).toContain('.text-center')
68+
})
69+
})
70+
71+
test('does not purge except in production', () => {
72+
const OLD_NODE_ENV = process.env.NODE_ENV
73+
process.env.NODE_ENV = 'development'
74+
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
75+
const input = fs.readFileSync(inputPath, 'utf8')
76+
77+
return postcss([
78+
tailwind({
79+
...defaultConfig,
80+
purge: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
81+
}),
82+
])
83+
.process(input, { from: inputPath })
84+
.then(result => {
85+
process.env.NODE_ENV = OLD_NODE_ENV
86+
const expected = fs.readFileSync(
87+
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
88+
'utf8'
89+
)
90+
91+
expect(result.css).toBe(expected)
92+
})
93+
})
94+
95+
test('purges outside of production if explicitly enabled', () => {
96+
const OLD_NODE_ENV = process.env.NODE_ENV
97+
process.env.NODE_ENV = 'development'
98+
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
99+
const input = fs.readFileSync(inputPath, 'utf8')
100+
101+
return postcss([
102+
tailwind({
103+
...config,
104+
purge: { enabled: true, content: [path.resolve(`${__dirname}/fixtures/**/*.html`)] },
105+
}),
106+
])
107+
.process(input, { from: inputPath })
108+
.then(result => {
109+
process.env.NODE_ENV = OLD_NODE_ENV
110+
111+
expect(result.css).not.toContain('.bg-red-600')
112+
expect(result.css).not.toContain('.w-1\\/3')
113+
expect(result.css).not.toContain('.flex')
114+
expect(result.css).not.toContain('.font-sans')
115+
expect(result.css).not.toContain('.text-right')
116+
expect(result.css).not.toContain('.px-4')
117+
expect(result.css).not.toContain('.h-full')
118+
119+
expect(result.css).toContain('.bg-red-500')
120+
expect(result.css).toContain('.md\\:bg-blue-300')
121+
expect(result.css).toContain('.w-1\\/2')
122+
expect(result.css).toContain('.block')
123+
expect(result.css).toContain('.md\\:flow-root')
124+
expect(result.css).toContain('.h-screen')
125+
expect(result.css).toContain('.min-h-\\(screen-4\\)')
126+
expect(result.css).toContain('.bg-black\\!')
127+
expect(result.css).toContain('.font-\\%\\#\\$\\@')
128+
expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
129+
expect(result.css).toContain('.inline-grid')
130+
expect(result.css).toContain('.grid-cols-3')
131+
expect(result.css).toContain('.px-1\\.5')
132+
expect(result.css).toContain('.col-span-2')
133+
expect(result.css).toContain('.col-span-1')
134+
expect(result.css).toContain('.text-center')
135+
})
136+
})
137+
138+
test('purgecss options can be provided', () => {
139+
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
140+
const input = fs.readFileSync(inputPath, 'utf8')
141+
142+
return postcss([
143+
tailwind({
144+
...config,
145+
purge: {
146+
enabled: true,
147+
options: {
148+
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
149+
whitelist: ['md:bg-green-500'],
150+
},
151+
},
152+
}),
153+
])
154+
.process(input, { from: inputPath })
155+
.then(result => {
156+
expect(result.css).not.toContain('.bg-red-600')
157+
expect(result.css).not.toContain('.w-1\\/3')
158+
expect(result.css).not.toContain('.flex')
159+
expect(result.css).not.toContain('.font-sans')
160+
expect(result.css).not.toContain('.text-right')
161+
expect(result.css).not.toContain('.px-4')
162+
expect(result.css).not.toContain('.h-full')
163+
164+
expect(result.css).toContain('.md\\:bg-green-500')
165+
expect(result.css).toContain('.bg-red-500')
166+
expect(result.css).toContain('.md\\:bg-blue-300')
167+
expect(result.css).toContain('.w-1\\/2')
168+
expect(result.css).toContain('.block')
169+
expect(result.css).toContain('.md\\:flow-root')
170+
expect(result.css).toContain('.h-screen')
171+
expect(result.css).toContain('.min-h-\\(screen-4\\)')
172+
expect(result.css).toContain('.bg-black\\!')
173+
expect(result.css).toContain('.font-\\%\\#\\$\\@')
174+
expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
175+
expect(result.css).toContain('.inline-grid')
176+
expect(result.css).toContain('.grid-cols-3')
177+
expect(result.css).toContain('.px-1\\.5')
178+
expect(result.css).toContain('.col-span-2')
179+
expect(result.css).toContain('.col-span-1')
180+
expect(result.css).toContain('.text-center')
181+
})
182+
})
183+
184+
test('can purge all CSS, not just Tailwind classes', () => {
185+
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
186+
const input = fs.readFileSync(inputPath, 'utf8')
187+
188+
return postcss([
189+
tailwind({
190+
...config,
191+
purge: {
192+
enabled: true,
193+
mode: 'all',
194+
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
195+
},
196+
}),
197+
function(css) {
198+
// Remove any comments to avoid accidentally asserting against them
199+
// instead of against real CSS rules.
200+
css.walkComments(c => c.remove())
201+
},
202+
])
203+
.process(input, { from: inputPath })
204+
.then(result => {
205+
expect(result.css).not.toContain('html')
206+
expect(result.css).not.toContain('body')
207+
expect(result.css).not.toContain('button')
208+
expect(result.css).not.toContain('legend')
209+
expect(result.css).not.toContain('progress')
210+
211+
expect(result.css).toContain('.bg-red-500')
212+
expect(result.css).toContain('.md\\:bg-blue-300')
213+
expect(result.css).toContain('.w-1\\/2')
214+
expect(result.css).toContain('.block')
215+
expect(result.css).toContain('.md\\:flow-root')
216+
expect(result.css).toContain('.h-screen')
217+
expect(result.css).toContain('.min-h-\\(screen-4\\)')
218+
expect(result.css).toContain('.bg-black\\!')
219+
expect(result.css).toContain('.font-\\%\\#\\$\\@')
220+
expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
221+
expect(result.css).toContain('.inline-grid')
222+
expect(result.css).toContain('.grid-cols-3')
223+
expect(result.css).toContain('.px-1\\.5')
224+
expect(result.css).toContain('.col-span-2')
225+
expect(result.css).toContain('.col-span-1')
226+
expect(result.css).toContain('.text-center')
227+
})
228+
})
229+
230+
test('the `conservative` mode can be set explicitly', () => {
231+
const OLD_NODE_ENV = process.env.NODE_ENV
232+
process.env.NODE_ENV = 'production'
233+
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
234+
const input = fs.readFileSync(inputPath, 'utf8')
235+
236+
return postcss([
237+
tailwind({
238+
...config,
239+
purge: {
240+
mode: 'conservative',
241+
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
242+
},
243+
}),
244+
])
245+
.process(input, { from: inputPath })
246+
.then(result => {
247+
process.env.NODE_ENV = OLD_NODE_ENV
248+
249+
expect(result.css).not.toContain('.bg-red-600')
250+
expect(result.css).not.toContain('.w-1\\/3')
251+
expect(result.css).not.toContain('.flex')
252+
expect(result.css).not.toContain('.font-sans')
253+
expect(result.css).not.toContain('.text-right')
254+
expect(result.css).not.toContain('.px-4')
255+
expect(result.css).not.toContain('.h-full')
256+
257+
expect(result.css).toContain('.bg-red-500')
258+
expect(result.css).toContain('.md\\:bg-blue-300')
259+
expect(result.css).toContain('.w-1\\/2')
260+
expect(result.css).toContain('.block')
261+
expect(result.css).toContain('.md\\:flow-root')
262+
expect(result.css).toContain('.h-screen')
263+
expect(result.css).toContain('.min-h-\\(screen-4\\)')
264+
expect(result.css).toContain('.bg-black\\!')
265+
expect(result.css).toContain('.font-\\%\\#\\$\\@')
266+
expect(result.css).toContain('.w-\\(1\\/2\\+8\\)')
267+
expect(result.css).toContain('.inline-grid')
268+
expect(result.css).toContain('.grid-cols-3')
269+
expect(result.css).toContain('.px-1\\.5')
270+
expect(result.css).toContain('.col-span-2')
271+
expect(result.css).toContain('.col-span-1')
272+
expect(result.css).toContain('.text-center')
273+
})
274+
})

__tests__/responsiveAtRule.test.js

+20
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ test('it can generate responsive variants', () => {
1212
.banana { color: yellow; }
1313
.chocolate { color: brown; }
1414
}
15+
16+
@tailwind screens;
1517
`
1618

1719
const output = `
@@ -52,6 +54,8 @@ test('it can generate responsive variants with a custom separator', () => {
5254
.banana { color: yellow; }
5355
.chocolate { color: brown; }
5456
}
57+
58+
@tailwind screens;
5559
`
5660

5761
const output = `
@@ -92,6 +96,8 @@ test('it can generate responsive variants when classes have non-standard charact
9296
.hover\\:banana { color: yellow; }
9397
.chocolate-2\\.5 { color: brown; }
9498
}
99+
100+
@tailwind screens;
95101
`
96102

97103
const output = `
@@ -137,6 +143,8 @@ test('responsive variants are grouped', () => {
137143
@responsive {
138144
.chocolate { color: brown; }
139145
}
146+
147+
@tailwind screens;
140148
`
141149

142150
const output = `
@@ -181,6 +189,8 @@ test('it can generate responsive variants for nested at-rules', () => {
181189
.grid\\:banana { color: blue; }
182190
}
183191
}
192+
193+
@tailwind screens;
184194
`
185195

186196
const output = `
@@ -244,6 +254,8 @@ test('it can generate responsive variants for deeply nested at-rules', () => {
244254
}
245255
}
246256
}
257+
258+
@tailwind screens;
247259
`
248260

249261
const output = `
@@ -307,6 +319,8 @@ test('screen prefix is only applied to the last class in a selector', () => {
307319
@responsive {
308320
.banana li * .sandwich #foo > div { color: yellow; }
309321
}
322+
323+
@tailwind screens;
310324
`
311325

312326
const output = `
@@ -342,6 +356,8 @@ test('responsive variants are generated for all selectors in a rule', () => {
342356
@responsive {
343357
.foo, .bar { color: yellow; }
344358
}
359+
360+
@tailwind screens;
345361
`
346362

347363
const output = `
@@ -377,6 +393,8 @@ test('selectors with no classes cannot be made responsive', () => {
377393
@responsive {
378394
div { color: yellow; }
379395
}
396+
397+
@tailwind screens;
380398
`
381399
expect.assertions(1)
382400
return run(input, {
@@ -398,6 +416,8 @@ test('all selectors in a rule must contain classes', () => {
398416
@responsive {
399417
.foo, div { color: yellow; }
400418
}
419+
420+
@tailwind screens;
401421
`
402422
expect.assertions(1)
403423
return run(input, {

0 commit comments

Comments
 (0)