tailwindcss/tests/purgeUnusedStyles.test.js
Robin Malfait bfc61625d9
[WIP] Unify JIT and AOT code paths (#4188)
* WIP

* WIP

* Finish combining JIT and AOT plugins

Still lots of clean up that can be done in some of the more complex ones, but at least it's one file per plugin now.

* Remove unused import

* Fix AOT generation bugs

* Move corePlugins/index.js to corePlugins.js

* Convert JIT files to ESM

* Move tests

* Reorder core plugins to match JIT order

* Update AOT apply tests

* Unify utils

* Combine plugin lists to one single source of truth

* Finish resolving merge conflicts, fix tests

Co-authored-by: Adam Wathan <adam.wathan@gmail.com>
2021-04-30 10:58:40 -04:00

765 lines
19 KiB
JavaScript

import fs from 'fs'
import path from 'path'
import postcss from 'postcss'
import tailwind from '../src/index'
import { tailwindExtractor } from '../src/lib/purgeUnusedStyles'
import defaultConfig from '../stubs/defaultConfig.stub.js'
function suppressConsoleLogs(cb, type = 'warn') {
return () => {
const spy = jest.spyOn(global.console, type).mockImplementation(jest.fn())
const promise = new Promise((resolve, reject) => {
Promise.resolve(cb()).then(resolve, reject)
})
promise.then(spy.mockRestore, spy.mockRestore)
return promise
}
}
function extractRules(root) {
let rules = []
root.walkRules((r) => {
rules = rules.concat(r.selectors)
})
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: {
extend: {
colors: {
'black!': '#000',
},
spacing: {
1.5: '0.375rem',
'(1/2+8)': 'calc(50% + 2rem)',
},
minHeight: {
'(screen-4)': 'calc(100vh - 1rem)',
},
fontFamily: {
'%#$@': 'Comic Sans',
},
},
},
}
delete config.presets
function assertPurged(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')
expect(result.css).toContain('.flow-root')
expect(result.css).toContain('.text-green-700')
expect(result.css).toContain('.bg-green-100')
expect(result.css).toContain('.text-left')
expect(result.css).toContain('.font-mono')
expect(result.css).toContain('.col-span-4')
expect(result.css).toContain('.tracking-tight')
expect(result.css).toContain('.whitespace-nowrap')
}
test('purges unused classes', () => {
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', () => {
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('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('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('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('extra purgecss control comments can be added manually', () => {
return inProduction(
suppressConsoleLogs(() => {
const input = `
@tailwind base;
/* purgecss start ignore */
.btn {
background: red;
}
/* purgecss end ignore */
@tailwind components;
@tailwind utilities;
`
return postcss([
tailwind({
...config,
purge: {
layers: ['utilities'],
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
},
}),
])
.process(input, { from: null })
.then((result) => {
const rules = extractRules(result.root)
expect(rules).toContain('.btn')
expect(rules).toContain('.container')
assertPurged(result)
})
})
)
})
test(
'does not purge except in production',
suppressConsoleLogs(() => {
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) => {
const expected = fs.readFileSync(
path.resolve(`${__dirname}/fixtures/tailwind-output.css`),
'utf8'
)
expect(result.css).toMatchCss(expected)
})
})
)
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).toMatchCss(expected)
})
})
)
})
test('does not purge if explicitly disabled', () => {
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).toMatchCss(expected)
})
})
)
})
test('does not purge if purge is simply false', () => {
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).toMatchCss(expected)
})
})
)
})
test('purges outside of production if explicitly enabled', () => {
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',
suppressConsoleLogs(() => {
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`)],
safelist: ['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`)],
},
}),
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)
})
})
)
test('element selectors are preserved by default', () => {
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('element selectors are preserved even when defaultExtractor is overridden', () => {
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: true,
options: {
defaultExtractor: tailwindExtractor,
},
},
}),
])
.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', () => {
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)
})
})
)
})