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:
parent
3569d49f7e
commit
8518fee9ea
8 changed files with 188 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
20
src/cli.js
20
src/cli.js
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue