Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement purge safelist #4580

Merged
merged 5 commits into from Jun 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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