Skip to content

Commit a8fe89d

Browse files
committed
implement safelist where you can use regex patterns
1 parent 0e36ecb commit a8fe89d

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed

src/lib/setupContextUtils.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import * as corePlugins from '../corePlugins'
1616
import * as sharedState from './sharedState'
1717
import { env } from './sharedState'
1818
import { toPath } from '../util/toPath'
19+
import log from '../util/log'
1920

2021
function insertInto(list, value, { before = [] } = {}) {
2122
before = [].concat(before)
@@ -532,6 +533,83 @@ function registerPlugins(plugins, context) {
532533
variantFunctions.map((variantFunction, idx) => [sort << BigInt(idx), variantFunction])
533534
)
534535
}
536+
537+
//
538+
let warnedAbout = new Set([])
539+
context.safelist = function () {
540+
let safelist = (context.tailwindConfig.safelist ?? []).filter(Boolean)
541+
if (safelist.length <= 0) return []
542+
543+
let output = []
544+
let checks = []
545+
546+
for (let value of safelist) {
547+
if (typeof value === 'string') {
548+
output.push(value)
549+
continue
550+
}
551+
552+
if (value instanceof RegExp) {
553+
if (!warnedAbout.has('root-regex')) {
554+
log.warn([
555+
// TODO: Improve this warning message
556+
'RegExp in the safelist option is not supported.',
557+
'Please use the object syntax instead: https://tailwindcss.com/docs/...',
558+
])
559+
warnedAbout.add('root-regex')
560+
}
561+
continue
562+
}
563+
564+
checks.push(value)
565+
}
566+
567+
if (checks.length <= 0) return output.map((value) => ({ raw: value, extension: 'html' }))
568+
569+
let patternMatchingCount = new Map()
570+
571+
for (let util of classList) {
572+
let utils = Array.isArray(util)
573+
? (() => {
574+
let [utilName, options] = util
575+
return Object.keys(options?.values ?? {}).map((value) => formatClass(utilName, value))
576+
})()
577+
: [util]
578+
579+
for (let util of utils) {
580+
for (let { pattern, variants = [] } of checks) {
581+
// RegExp with the /g flag are stateful, so let's reset the last
582+
// index pointer to reset the state.
583+
pattern.lastIndex = 0
584+
585+
if (!patternMatchingCount.has(pattern)) {
586+
patternMatchingCount.set(pattern, 0)
587+
}
588+
589+
if (!pattern.test(util)) continue
590+
591+
patternMatchingCount.set(pattern, patternMatchingCount.get(pattern) + 1)
592+
593+
output.push(util)
594+
for (let variant of variants) {
595+
output.push(variant + context.tailwindConfig.separator + util)
596+
}
597+
}
598+
}
599+
}
600+
601+
for (let [regex, count] of patternMatchingCount.entries()) {
602+
if (count !== 0) continue
603+
604+
log.warn([
605+
// TODO: Improve this warning message
606+
`You have a regex pattern in your "safelist" config (${regex}) that doesn't match any utilities.`,
607+
'For more info, visit https://tailwindcss.com/docs/...',
608+
])
609+
}
610+
611+
return output.map((value) => ({ raw: value, extension: 'html' }))
612+
}
535613
}
536614

537615
export function createContext(

src/lib/setupTrackingContext.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
8080
let changedContent = context.tailwindConfig.content.content
8181
.filter((item) => typeof item.raw === 'string')
8282
.concat(context.tailwindConfig.content.safelist)
83+
.concat(context.safelist())
8384
.map(({ raw, extension }) => ({ content: raw, extension }))
8485

8586
for (let changedFile of resolveChangedFiles(candidateFiles, fileModifiedMap)) {

src/lib/setupWatchingContext.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ function resolvedChangedContent(context, candidateFiles) {
188188
let changedContent = context.tailwindConfig.content.content
189189
.filter((item) => typeof item.raw === 'string')
190190
.concat(context.tailwindConfig.content.safelist)
191+
.concat(context.safelist())
191192
.map(({ raw, extension }) => ({ content: raw, extension }))
192193

193194
for (let changedFile of resolveChangedFiles(context, candidateFiles)) {

tests/safelist.test.js

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
import { run, html, css } from './util/run'
2+
3+
it('should not safelist anything', () => {
4+
let config = {
5+
content: [{ raw: html`<div class="uppercase"></div>` }],
6+
}
7+
8+
return run('@tailwind utilities', config).then((result) => {
9+
return expect(result.css).toMatchCss(css`
10+
.uppercase {
11+
text-transform: uppercase;
12+
}
13+
`)
14+
})
15+
})
16+
17+
it('should safelist strings', () => {
18+
let config = {
19+
content: [{ raw: html`<div class="uppercase"></div>` }],
20+
safelist: ['mt-[20px]', 'font-bold', 'text-gray-200', 'hover:underline'],
21+
}
22+
23+
return run('@tailwind utilities', config).then((result) => {
24+
return expect(result.css).toMatchCss(css`
25+
.mt-\\[20px\\] {
26+
margin-top: 20px;
27+
}
28+
29+
.font-bold {
30+
font-weight: 700;
31+
}
32+
33+
.uppercase {
34+
text-transform: uppercase;
35+
}
36+
37+
.text-gray-200 {
38+
--tw-text-opacity: 1;
39+
color: rgb(229 231 235 / var(--tw-text-opacity));
40+
}
41+
42+
.hover\\:underline:hover {
43+
text-decoration: underline;
44+
}
45+
`)
46+
})
47+
})
48+
49+
it('should safelist based on a pattern regex', () => {
50+
let config = {
51+
content: [{ raw: html`<div class="uppercase"></div>` }],
52+
safelist: [
53+
{
54+
pattern: /bg-(red)-(100|200)/,
55+
variants: ['hover'],
56+
},
57+
],
58+
}
59+
60+
return run('@tailwind utilities', config).then((result) => {
61+
return expect(result.css).toMatchCss(css`
62+
.bg-red-100 {
63+
--tw-bg-opacity: 1;
64+
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
65+
}
66+
67+
.bg-red-200 {
68+
--tw-bg-opacity: 1;
69+
background-color: rgb(254 202 202 / var(--tw-bg-opacity));
70+
}
71+
72+
.uppercase {
73+
text-transform: uppercase;
74+
}
75+
76+
.hover\\:bg-red-100:hover {
77+
--tw-bg-opacity: 1;
78+
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
79+
}
80+
81+
.hover\\:bg-red-200:hover {
82+
--tw-bg-opacity: 1;
83+
background-color: rgb(254 202 202 / var(--tw-bg-opacity));
84+
}
85+
`)
86+
})
87+
})
88+
89+
it('should not generate duplicates', () => {
90+
let config = {
91+
content: [{ raw: html`<div class="uppercase"></div>` }],
92+
safelist: [
93+
'uppercase',
94+
{
95+
pattern: /bg-(red)-(100|200)/,
96+
variants: ['hover'],
97+
},
98+
{
99+
pattern: /bg-(red)-(100|200)/,
100+
variants: ['hover'],
101+
},
102+
{
103+
pattern: /bg-(red)-(100|200)/,
104+
variants: ['hover'],
105+
},
106+
],
107+
}
108+
109+
return run('@tailwind utilities', config).then((result) => {
110+
return expect(result.css).toMatchCss(css`
111+
.bg-red-100 {
112+
--tw-bg-opacity: 1;
113+
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
114+
}
115+
116+
.bg-red-200 {
117+
--tw-bg-opacity: 1;
118+
background-color: rgb(254 202 202 / var(--tw-bg-opacity));
119+
}
120+
121+
.uppercase {
122+
text-transform: uppercase;
123+
}
124+
125+
.hover\\:bg-red-100:hover {
126+
--tw-bg-opacity: 1;
127+
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
128+
}
129+
130+
.hover\\:bg-red-200:hover {
131+
--tw-bg-opacity: 1;
132+
background-color: rgb(254 202 202 / var(--tw-bg-opacity));
133+
}
134+
`)
135+
})
136+
})
137+
138+
it('should safelist when using a custom prefix', () => {
139+
let config = {
140+
prefix: 'tw-',
141+
content: [{ raw: html`<div class="tw-uppercase"></div>` }],
142+
safelist: [
143+
{
144+
pattern: /tw-bg-red-(100|200)/g,
145+
},
146+
],
147+
}
148+
149+
return run('@tailwind utilities', config).then((result) => {
150+
return expect(result.css).toMatchCss(css`
151+
.tw-bg-red-100 {
152+
--tw-bg-opacity: 1;
153+
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
154+
}
155+
156+
.tw-bg-red-200 {
157+
--tw-bg-opacity: 1;
158+
background-color: rgb(254 202 202 / var(--tw-bg-opacity));
159+
}
160+
161+
.tw-uppercase {
162+
text-transform: uppercase;
163+
}
164+
`)
165+
})
166+
})
167+
168+
it('should not safelist when an empty list is provided', () => {
169+
let config = {
170+
content: [{ raw: html`<div class="uppercase"></div>` }],
171+
safelist: [],
172+
}
173+
174+
return run('@tailwind utilities', config).then((result) => {
175+
return expect(result.css).toMatchCss(css`
176+
.uppercase {
177+
text-transform: uppercase;
178+
}
179+
`)
180+
})
181+
})
182+
183+
it('should not safelist when an sparse/holey list is provided', () => {
184+
let config = {
185+
content: [{ raw: html`<div class="uppercase"></div>` }],
186+
safelist: [, , ,],
187+
}
188+
189+
return run('@tailwind utilities', config).then((result) => {
190+
return expect(result.css).toMatchCss(css`
191+
.uppercase {
192+
text-transform: uppercase;
193+
}
194+
`)
195+
})
196+
})

0 commit comments

Comments
 (0)