From 88e7bd028f8c9cdcbd52968e631b532a986bef99 Mon Sep 17 00:00:00 2001 From: Vincent Lemeunier Date: Sat, 20 Mar 2021 14:15:37 +0100 Subject: [PATCH] Allow custom extractors --- src/lib/expandTailwindAtRules.js | 49 ++++++++++++++++------ tests/11-custom-extractors.test.css | 17 ++++++++ tests/11-custom-extractors.test.html | 14 +++++++ tests/11-custom-extractors.test.js | 63 ++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 tests/11-custom-extractors.test.css create mode 100644 tests/11-custom-extractors.test.html create mode 100644 tests/11-custom-extractors.test.js diff --git a/src/lib/expandTailwindAtRules.js b/src/lib/expandTailwindAtRules.js index 0d6ed2a..8a0bfa6 100644 --- a/src/lib/expandTailwindAtRules.js +++ b/src/lib/expandTailwindAtRules.js @@ -1,4 +1,5 @@ const fs = require('fs') +const path = require('path') const fastGlob = require('fast-glob') const sharedState = require('./sharedState') const { generateRules } = require('./generateRules') @@ -7,10 +8,39 @@ const { bigSign } = require('./utils') let env = sharedState.env let contentMatchCache = sharedState.contentMatchCache +const BROAD_MATCH_GLOBAL_REGEXP = /[^<>"'`\s]*[^<>"'`\s:]/g +const INNER_MATCH_GLOBAL_REGEXP = /[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g + +function defaultJitExtractor(content) { + let broadMatches = content.match(BROAD_MATCH_GLOBAL_REGEXP) || [] + let innerMatches = content.match(INNER_MATCH_GLOBAL_REGEXP) || [] + + return [...broadMatches, ...innerMatches] +} + +function getExtractor(fileName, tailwindConfig) { + const purgeOptions = tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.options + + if (!purgeOptions) { + return defaultJitExtractor + } + + const fileExtension = path.extname(fileName).slice(1) + const fileSpecificExtractor = (purgeOptions.extractors || []).find((extractor) => + extractor.extensions.includes(fileExtension) + ) + + if (fileSpecificExtractor) { + return fileSpecificExtractor.extractor + } + + return purgeOptions.defaultExtractor || defaultJitExtractor +} + // Scans template contents for possible classes. This is a hot path on initial build but // not too important for subsequent builds. The faster the better though — if we can speed // up these regexes by 50% that could cut initial build time by like 20%. -function getClassCandidates(content, contentMatchCache, candidates, seen) { +function getClassCandidates(content, extractor, contentMatchCache, candidates, seen) { for (let line of content.split('\n')) { line = line.trim() @@ -24,20 +54,14 @@ function getClassCandidates(content, contentMatchCache, candidates, seen) { candidates.add(match) } } else { - let allMatches = new Set() - let broadMatches = line.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [] - let innerMatches = line.match(/[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g) || [] + let extractorMatches = extractor(line) + let lineMatchesSet = new Set(extractorMatches) - for (let match of broadMatches) { - allMatches.add(match) - candidates.add(match) - } - for (let match of innerMatches) { - allMatches.add(match) + for (let match of lineMatchesSet) { candidates.add(match) } - contentMatchCache.set(line, allMatches) + contentMatchCache.set(line, lineMatchesSet) } } } @@ -143,7 +167,8 @@ function expandTailwindAtRules(context, registerDependency) { env.DEBUG && console.time('Reading changed files') for (let file of context.changedFiles) { let content = fs.readFileSync(file, 'utf8') - getClassCandidates(content, contentMatchCache, candidates, seen) + let extractor = getExtractor(file, context.tailwindConfig) + getClassCandidates(content, extractor, contentMatchCache, candidates, seen) } env.DEBUG && console.timeEnd('Reading changed files') diff --git a/tests/11-custom-extractors.test.css b/tests/11-custom-extractors.test.css new file mode 100644 index 0000000..423463d --- /dev/null +++ b/tests/11-custom-extractors.test.css @@ -0,0 +1,17 @@ +* { + --tw-shadow: 0 0 #0000; + --tw-ring-inset: var(--tw-empty, /*!*/ /*!*/); + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgba(59, 130, 246, 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; +} +.bg-white { + --tw-bg-opacity: 1; + background-color: rgba(255, 255, 255, var(--tw-bg-opacity)); +} +.text-indigo-500 { + --tw-text-opacity: 1; + color: rgba(99, 102, 241, var(--tw-text-opacity)); +} diff --git a/tests/11-custom-extractors.test.html b/tests/11-custom-extractors.test.html new file mode 100644 index 0000000..871b099 --- /dev/null +++ b/tests/11-custom-extractors.test.html @@ -0,0 +1,14 @@ + + + + + + + Title + + + +
hello world
+ text-red-500 shouldn't appear in the output + + diff --git a/tests/11-custom-extractors.test.js b/tests/11-custom-extractors.test.js new file mode 100644 index 0000000..07e475f --- /dev/null +++ b/tests/11-custom-extractors.test.js @@ -0,0 +1,63 @@ +const postcss = require('postcss') +const fs = require('fs') +const path = require('path') + +function run(input, config = {}) { + jest.resetModules() + const tailwind = require('../src/index.js') + return postcss([tailwind(config)]).process(input, { from: path.resolve(__filename) }) +} + +function customExtractor(content) { + const matches = content.match(/class="([^"]+)"/) + return matches ? matches[1].split(/\s+/) : [] +} + +const css = ` + @tailwind base; + @tailwind components; + @tailwind utilities; +` +const expectedPath = path.resolve(__dirname, './11-custom-extractors.test.css') +const expected = fs.readFileSync(expectedPath, 'utf8') + +test('defaultExtractor', () => { + let config = { + purge: { + content: [path.resolve(__dirname, './11-custom-extractors.test.html')], + options: { + defaultExtractor: customExtractor, + }, + }, + corePlugins: { preflight: false }, + theme: {}, + plugins: [], + } + + return run(css, config).then((result) => { + expect(result.css).toMatchCss(expected) + }) +}) + +test('extractors array', () => { + let config = { + purge: { + content: [path.resolve(__dirname, './11-custom-extractors.test.html')], + options: { + extractors: [ + { + extractor: customExtractor, + extensions: ['html'], + }, + ], + }, + }, + corePlugins: { preflight: false }, + theme: {}, + plugins: [], + } + + return run(css, config).then((result) => { + expect(result.css).toMatchCss(expected) + }) +})