Skip to content

Commit 88e7bd0

Browse files
committed
Allow custom extractors
1 parent 1529bf1 commit 88e7bd0

4 files changed

+131
-12
lines changed

src/lib/expandTailwindAtRules.js

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const fs = require('fs')
2+
const path = require('path')
23
const fastGlob = require('fast-glob')
34
const sharedState = require('./sharedState')
45
const { generateRules } = require('./generateRules')
@@ -7,10 +8,39 @@ const { bigSign } = require('./utils')
78
let env = sharedState.env
89
let contentMatchCache = sharedState.contentMatchCache
910

11+
const BROAD_MATCH_GLOBAL_REGEXP = /[^<>"'`\s]*[^<>"'`\s:]/g
12+
const INNER_MATCH_GLOBAL_REGEXP = /[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g
13+
14+
function defaultJitExtractor(content) {
15+
let broadMatches = content.match(BROAD_MATCH_GLOBAL_REGEXP) || []
16+
let innerMatches = content.match(INNER_MATCH_GLOBAL_REGEXP) || []
17+
18+
return [...broadMatches, ...innerMatches]
19+
}
20+
21+
function getExtractor(fileName, tailwindConfig) {
22+
const purgeOptions = tailwindConfig && tailwindConfig.purge && tailwindConfig.purge.options
23+
24+
if (!purgeOptions) {
25+
return defaultJitExtractor
26+
}
27+
28+
const fileExtension = path.extname(fileName).slice(1)
29+
const fileSpecificExtractor = (purgeOptions.extractors || []).find((extractor) =>
30+
extractor.extensions.includes(fileExtension)
31+
)
32+
33+
if (fileSpecificExtractor) {
34+
return fileSpecificExtractor.extractor
35+
}
36+
37+
return purgeOptions.defaultExtractor || defaultJitExtractor
38+
}
39+
1040
// Scans template contents for possible classes. This is a hot path on initial build but
1141
// not too important for subsequent builds. The faster the better though — if we can speed
1242
// up these regexes by 50% that could cut initial build time by like 20%.
13-
function getClassCandidates(content, contentMatchCache, candidates, seen) {
43+
function getClassCandidates(content, extractor, contentMatchCache, candidates, seen) {
1444
for (let line of content.split('\n')) {
1545
line = line.trim()
1646

@@ -24,20 +54,14 @@ function getClassCandidates(content, contentMatchCache, candidates, seen) {
2454
candidates.add(match)
2555
}
2656
} else {
27-
let allMatches = new Set()
28-
let broadMatches = line.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []
29-
let innerMatches = line.match(/[^<>"'`\s.(){}[\]#=%]*[^<>"'`\s.(){}[\]#=%:]/g) || []
57+
let extractorMatches = extractor(line)
58+
let lineMatchesSet = new Set(extractorMatches)
3059

31-
for (let match of broadMatches) {
32-
allMatches.add(match)
33-
candidates.add(match)
34-
}
35-
for (let match of innerMatches) {
36-
allMatches.add(match)
60+
for (let match of lineMatchesSet) {
3761
candidates.add(match)
3862
}
3963

40-
contentMatchCache.set(line, allMatches)
64+
contentMatchCache.set(line, lineMatchesSet)
4165
}
4266
}
4367
}
@@ -143,7 +167,8 @@ function expandTailwindAtRules(context, registerDependency) {
143167
env.DEBUG && console.time('Reading changed files')
144168
for (let file of context.changedFiles) {
145169
let content = fs.readFileSync(file, 'utf8')
146-
getClassCandidates(content, contentMatchCache, candidates, seen)
170+
let extractor = getExtractor(file, context.tailwindConfig)
171+
getClassCandidates(content, extractor, contentMatchCache, candidates, seen)
147172
}
148173
env.DEBUG && console.timeEnd('Reading changed files')
149174

tests/11-custom-extractors.test.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
* {
2+
--tw-shadow: 0 0 #0000;
3+
--tw-ring-inset: var(--tw-empty, /*!*/ /*!*/);
4+
--tw-ring-offset-width: 0px;
5+
--tw-ring-offset-color: #fff;
6+
--tw-ring-color: rgba(59, 130, 246, 0.5);
7+
--tw-ring-offset-shadow: 0 0 #0000;
8+
--tw-ring-shadow: 0 0 #0000;
9+
}
10+
.bg-white {
11+
--tw-bg-opacity: 1;
12+
background-color: rgba(255, 255, 255, var(--tw-bg-opacity));
13+
}
14+
.text-indigo-500 {
15+
--tw-text-opacity: 1;
16+
color: rgba(99, 102, 241, var(--tw-text-opacity));
17+
}

tests/11-custom-extractors.test.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" href="/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Title</title>
8+
<link rel="stylesheet" href="./tailwind.css" />
9+
</head>
10+
<body>
11+
<div class="text-indigo-500 bg-white">hello world</div>
12+
<span>text-red-500 shouldn't appear in the output</span>
13+
</body>
14+
</html>

tests/11-custom-extractors.test.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const postcss = require('postcss')
2+
const fs = require('fs')
3+
const path = require('path')
4+
5+
function run(input, config = {}) {
6+
jest.resetModules()
7+
const tailwind = require('../src/index.js')
8+
return postcss([tailwind(config)]).process(input, { from: path.resolve(__filename) })
9+
}
10+
11+
function customExtractor(content) {
12+
const matches = content.match(/class="([^"]+)"/)
13+
return matches ? matches[1].split(/\s+/) : []
14+
}
15+
16+
const css = `
17+
@tailwind base;
18+
@tailwind components;
19+
@tailwind utilities;
20+
`
21+
const expectedPath = path.resolve(__dirname, './11-custom-extractors.test.css')
22+
const expected = fs.readFileSync(expectedPath, 'utf8')
23+
24+
test('defaultExtractor', () => {
25+
let config = {
26+
purge: {
27+
content: [path.resolve(__dirname, './11-custom-extractors.test.html')],
28+
options: {
29+
defaultExtractor: customExtractor,
30+
},
31+
},
32+
corePlugins: { preflight: false },
33+
theme: {},
34+
plugins: [],
35+
}
36+
37+
return run(css, config).then((result) => {
38+
expect(result.css).toMatchCss(expected)
39+
})
40+
})
41+
42+
test('extractors array', () => {
43+
let config = {
44+
purge: {
45+
content: [path.resolve(__dirname, './11-custom-extractors.test.html')],
46+
options: {
47+
extractors: [
48+
{
49+
extractor: customExtractor,
50+
extensions: ['html'],
51+
},
52+
],
53+
},
54+
},
55+
corePlugins: { preflight: false },
56+
theme: {},
57+
plugins: [],
58+
}
59+
60+
return run(css, config).then((result) => {
61+
expect(result.css).toMatchCss(expected)
62+
})
63+
})

0 commit comments

Comments
 (0)