diff --git a/__tests__/purgeUnusedStyles.test.js b/__tests__/purgeUnusedStyles.test.js index 5b532217fb6c..3fbc1453460f 100644 --- a/__tests__/purgeUnusedStyles.test.js +++ b/__tests__/purgeUnusedStyles.test.js @@ -2,6 +2,7 @@ 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') { @@ -658,6 +659,60 @@ test('element selectors are preserved by default', () => { ) }) +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(() => { diff --git a/src/lib/purgeUnusedStyles.js b/src/lib/purgeUnusedStyles.js index 92d075c46682..e75da457d564 100644 --- a/src/lib/purgeUnusedStyles.js +++ b/src/lib/purgeUnusedStyles.js @@ -22,6 +22,17 @@ function removeTailwindMarkers(css) { }) } +export function tailwindExtractor(content) { + // Capture as liberally as possible, including things like `h-(screen-1.5)` + const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [] + const broadMatchesWithoutTrailingSlash = broadMatches.map((match) => _.trimEnd(match, '\\')) + + // Capture classes within other delimiters like .block(class="w-1/2") in Pug + const innerMatches = content.match(/[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g) || [] + + return broadMatches.concat(broadMatchesWithoutTrailingSlash).concat(innerMatches) +} + export default function purgeUnusedUtilities(config, configChanged) { const purgeEnabled = _.get( config, @@ -46,6 +57,8 @@ export default function purgeUnusedUtilities(config, configChanged) { return removeTailwindMarkers } + const { defaultExtractor, ...purgeOptions } = config.purge.options || {} + return postcss([ function (css) { const mode = _.get(config, 'purge.mode', 'layers') @@ -93,22 +106,16 @@ export default function purgeUnusedUtilities(config, configChanged) { purgecss({ content: Array.isArray(config.purge) ? config.purge : config.purge.content, defaultExtractor: (content) => { - // Capture as liberally as possible, including things like `h-(screen-1.5)` - const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [] - const broadMatchesWithoutTrailingSlash = broadMatches.map((match) => _.trimEnd(match, '\\')) - - // Capture classes within other delimiters like .block(class="w-1/2") in Pug - const innerMatches = content.match(/[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g) || [] - - const matches = broadMatches.concat(broadMatchesWithoutTrailingSlash).concat(innerMatches) + const extractor = defaultExtractor || tailwindExtractor + const preserved = [...extractor(content)] if (_.get(config, 'purge.preserveHtmlElements', true)) { - return [...htmlTags].concat(matches) - } else { - return matches + preserved.push(...htmlTags) } + + return preserved }, - ...config.purge.options, + ...purgeOptions, }), ]) }