Skip to content
This repository was archived by the owner on Apr 6, 2021. It is now read-only.

feat: allow custom extractors #125

Merged
merged 1 commit into from
Mar 23, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 37 additions & 12 deletions src/lib/expandTailwindAtRules.js
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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()

Expand All @@ -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)
}
}
}
Expand Down Expand Up @@ -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')

Expand Down
17 changes: 17 additions & 0 deletions tests/11-custom-extractors.test.css
Original file line number Diff line number Diff line change
@@ -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));
}
14 changes: 14 additions & 0 deletions tests/11-custom-extractors.test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<link rel="stylesheet" href="./tailwind.css" />
</head>
<body>
<div class="text-indigo-500 bg-white">hello world</div>
<span>text-red-500 shouldn't appear in the output</span>
</body>
</html>
63 changes: 63 additions & 0 deletions tests/11-custom-extractors.test.js
Original file line number Diff line number Diff line change
@@ -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')
Comment on lines +6 to +7
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've noticed that the second test in that file would pass even when it shouldn't, as long as the first one passed. I think it's due to this module sharedState which survives across multiple call in the same process.

That's why I'm calling resetModules, which resets require's cache and gives me a fresh new instance on each run.

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)
})
})