Skip to content

Commit

Permalink
implement purge safelist (#4580)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
RobinMalfait committed Jun 9, 2021
1 parent 3569d49 commit 8518fee
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 2 deletions.
2 changes: 1 addition & 1 deletion integrations/tailwindcss-cli/tests/cli.test.js
Expand Up @@ -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
Expand Down
52 changes: 52 additions & 0 deletions integrations/tailwindcss-cli/tests/integration.test.js
Expand Up @@ -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', () => {
Expand Down
52 changes: 52 additions & 0 deletions integrations/webpack-5/tests/integration.test.js
Expand Up @@ -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 changes: 19 additions & 1 deletion src/cli.js
Expand Up @@ -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) {
Expand Down
17 changes: 17 additions & 0 deletions src/jit/lib/setupTrackingContext.js
Expand Up @@ -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)) {
Expand Down
17 changes: 17 additions & 0 deletions src/jit/lib/setupWatchingContext.js
Expand Up @@ -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)) {
Expand Down
4 changes: 4 additions & 0 deletions src/lib/purgeUnusedStyles.js
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions tests/purgeUnusedStyles.test.js
Expand Up @@ -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(() => {
Expand Down

0 comments on commit 8518fee

Please sign in to comment.