implement purge safelist (#4580)

* fix --help output in tests

* add tests to ensure we can use `purge.safelist`

* implement the `purge.safelist` for strings

* proxy `purge.safelist` to `purge.options.safelist`

This allows us to have a similar API in `AOT` and `JIT` mode.

* only proxy `purge.safelist` to `purge.options.safelist` if
`purge.options.safelist` doesn't exists yet.
This commit is contained in:
Robin Malfait 2021-06-09 16:00:09 +02:00 • committed by GitHub
parent 3569d49f7e
commit 8518fee9ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 188 additions and 2 deletions

View file

@ -172,7 +172,7 @@ describe('Build command', () => {
-o, --output Output file
-w, --watch Watch for changes and rebuild as needed
--jit Build using JIT mode
--files Template files to scan for class names
--purge Content paths to use for removing unused classes
--postcss Load custom PostCSS configuration
-m, --minify Minify the output
-c, --config Path to a custom config file

View file

@ -25,6 +25,58 @@ describe('static build', () => {
`
)
})
it('should safelist a list of classes to always include', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
await writeInputFile(
'../tailwind.config.js',
javascript`
module.exports = {
purge: {
content: ['./src/index.html'],
safelist: ['bg-red-500','bg-red-600']
},
mode: 'jit',
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},
plugins: [],
}
`
)
$('node ../../lib/cli.js -i ./src/index.css -o ./dist/main.css', {
env: { NODE_ENV: 'production' },
})
await waitForOutputFileCreation('main.css')
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgba(220, 38, 38, var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
`
)
})
})
describe('watcher', () => {

View file

@ -227,4 +227,56 @@ describe.each([{ TAILWIND_MODE: 'watch' }, { TAILWIND_MODE: undefined }])('watch
return runningProcess.stop()
})
it('should safelist a list of classes to always include', async () => {
await writeInputFile('index.html', html`<div class="font-bold"></div>`)
await writeInputFile(
'../tailwind.config.js',
javascript`
module.exports = {
purge: {
content: ['./src/index.html'],
safelist: ['bg-red-500','bg-red-600']
},
mode: 'jit',
darkMode: false, // or 'media' or 'class'
theme: {
extend: {
},
},
variants: {
extend: {},
},
corePlugins: {
preflight: false,
},
plugins: [],
}
`
)
let runningProcess = $('webpack --mode=development --watch', { env })
await waitForOutputFileCreation('main.css')
expect(await readOutputFile('main.css')).toIncludeCss(
css`
.bg-red-500 {
--tw-bg-opacity: 1;
background-color: rgba(239, 68, 68, var(--tw-bg-opacity));
}
.bg-red-600 {
--tw-bg-opacity: 1;
background-color: rgba(220, 38, 38, var(--tw-bg-opacity));
}
.font-bold {
font-weight: 700;
}
`
)
return runningProcess.stop()
})
})

View file

@ -356,7 +356,25 @@ async function build() {
}
function extractContent(config) {
return Array.isArray(config.purge) ? config.purge : config.purge.content
let content = Array.isArray(config.purge) ? config.purge : config.purge.content
return content.concat(
(config.purge?.safelist ?? []).map((content) => {
if (typeof content === 'string') {
return { raw: content, extension: 'html' }
}
if (content instanceof RegExp) {
throw new Error(
"Values inside 'purge.safelist' can only be of type 'string', found 'regex'."
)
}
throw new Error(
`Values inside 'purge.safelist' can only be of type 'string', found '${typeof content}'.`
)
})
)
}
function extractFileGlobs(config) {

View file

@ -88,6 +88,23 @@ function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
: context.tailwindConfig.purge.content
)
.filter((item) => typeof item.raw === 'string')
.concat(
(context.tailwindConfig.purge?.safelist ?? []).map((content) => {
if (typeof content === 'string') {
return { raw: content, extension: 'html' }
}
if (content instanceof RegExp) {
throw new Error(
"Values inside 'purge.safelist' can only be of type 'string', found 'regex'."
)
}
throw new Error(
`Values inside 'purge.safelist' can only be of type 'string', found '${typeof content}'.`
)
})
)
.map(({ raw, extension }) => ({ content: raw, extension }))
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {

View file

@ -235,6 +235,23 @@ function resolvedChangedContent(context, candidateFiles) {
: context.tailwindConfig.purge.content
)
.filter((item) => typeof item.raw === 'string')
.concat(
(context.tailwindConfig.purge?.safelist ?? []).map((content) => {
if (typeof content === 'string') {
return { raw: content, extension: 'html' }
}
if (content instanceof RegExp) {
throw new Error(
"Values inside 'purge.safelist' can only be of type 'string', found 'regex'."
)
}
throw new Error(
`Values inside 'purge.safelist' can only be of type 'string', found '${typeof content}'.`
)
})
)
.map(({ raw, extension }) => ({ content: raw, extension }))
for (let changedFile of resolveChangedFiles(context, candidateFiles)) {

View file

@ -81,6 +81,10 @@ export default function purgeUnusedUtilities(
const transformers = config.purge.transform || {}
let { defaultExtractor: originalDefaultExtractor, ...purgeOptions } = config.purge.options || {}
if (config.purge?.safelist && !purgeOptions.hasOwnProperty('safelist')) {
purgeOptions.safelist = config.purge.safelist
}
if (!originalDefaultExtractor) {
originalDefaultExtractor =
typeof extractors === 'function' ? extractors : extractors.DEFAULT || tailwindExtractor

View file

@ -579,6 +579,32 @@ test(
})
)
test(
'proxying purge.safelist to purge.options.safelist works',
suppressConsoleLogs(() => {
const inputPath = path.resolve(`${__dirname}/fixtures/tailwind-input.css`)
const input = fs.readFileSync(inputPath, 'utf8')
return postcss([
tailwind({
...config,
purge: {
enabled: true,
safelist: ['md:bg-green-500'],
options: {
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
},
},
}),
])
.process(input, { from: withTestName(inputPath) })
.then((result) => {
expect(result.css).toContain('.md\\:bg-green-500')
assertPurged(result)
})
})
)
test(
'can purge all CSS, not just Tailwind classes',
suppressConsoleLogs(() => {