Skip to content

Commit 01fbe19

Browse files
Fix negative utility generation and detection when using a prefix (#7295)
* Add failing tests for negative utility detection We're not generating them properly in all cases, when using at-apply we sometimes crash, and safelisting doesn't currently work as expected. * Refactor * Generate utilities for negatives before and after the prefix * Properly detect negative utilities with prefixes in the safelist * Refactor test a bit * Add class list tests * Update changelog
1 parent ab9fd95 commit 01fbe19

6 files changed

+384
-23
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Correctly parse shadow lengths without a leading zero ([#7289](https://github.com/tailwindlabs/tailwindcss/pull/7289))
1414
- Don't crash when scanning extremely long class candidates ([#7331](https://github.com/tailwindlabs/tailwindcss/pull/7331))
1515
- Use less hacky fix for urls detected as custom properties ([#7275](https://github.com/tailwindlabs/tailwindcss/pull/7275))
16+
- Correctly generate negative utilities when dash is before the prefix ([#7295](https://github.com/tailwindlabs/tailwindcss/pull/7295))
17+
- Detect prefixed negative utilities in the safelist ([#7295](https://github.com/tailwindlabs/tailwindcss/pull/7295))
1618

1719
## [3.0.18] - 2022-01-28
1820

src/lib/generateRules.js

+26-2
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,23 @@ function applyPrefix(matches, context) {
6363
let [meta] = match
6464
if (meta.options.respectPrefix) {
6565
let container = postcss.root({ nodes: [match[1].clone()] })
66+
let classCandidate = match[1].raws.tailwind.classCandidate
67+
6668
container.walkRules((r) => {
67-
r.selector = prefixSelector(context.tailwindConfig.prefix, r.selector)
69+
// If this is a negative utility with a dash *before* the prefix we
70+
// have to ensure that the generated selector matches the candidate
71+
72+
// Not doing this will cause `-tw-top-1` to generate the class `.tw--top-1`
73+
// The disconnect between candidate <-> class can cause @apply to hard crash.
74+
let shouldPrependNegative = classCandidate.startsWith('-')
75+
76+
r.selector = prefixSelector(
77+
context.tailwindConfig.prefix,
78+
r.selector,
79+
shouldPrependNegative
80+
)
6881
})
82+
6983
match[1] = container.nodes[0]
7084
}
7185
}
@@ -371,6 +385,14 @@ function splitWithSeparator(input, separator) {
371385
return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g'))
372386
}
373387

388+
function* recordCandidates(matches, classCandidate) {
389+
for (const match of matches) {
390+
match[1].raws.tailwind = { classCandidate }
391+
392+
yield match
393+
}
394+
}
395+
374396
function* resolveMatches(candidate, context) {
375397
let separator = context.tailwindConfig.separator
376398
let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
@@ -482,7 +504,9 @@ function* resolveMatches(candidate, context) {
482504
continue
483505
}
484506

485-
matches = applyPrefix(matches.flat(), context)
507+
matches = matches.flat()
508+
matches = Array.from(recordCandidates(matches, classCandidate))
509+
matches = applyPrefix(matches, context)
486510

487511
if (important) {
488512
matches = applyImportant(matches, context)

src/lib/setupContextUtils.js

+16-3
Original file line numberDiff line numberDiff line change
@@ -666,17 +666,30 @@ function registerPlugins(plugins, context) {
666666

667667
if (checks.length > 0) {
668668
let patternMatchingCount = new Map()
669+
let prefixLength = context.tailwindConfig.prefix.length
669670

670671
for (let util of classList) {
671672
let utils = Array.isArray(util)
672673
? (() => {
673674
let [utilName, options] = util
674-
let classes = Object.keys(options?.values ?? {}).map((value) =>
675-
formatClass(utilName, value)
676-
)
675+
let values = Object.keys(options?.values ?? {})
676+
let classes = values.map((value) => formatClass(utilName, value))
677677

678678
if (options?.supportsNegativeValues) {
679+
// This is the normal negated version
680+
// e.g. `-inset-1` or `-tw-inset-1`
679681
classes = [...classes, ...classes.map((cls) => '-' + cls)]
682+
683+
// This is the negated version *after* the prefix
684+
// e.g. `tw--inset-1`
685+
// The prefix is already attached to util name
686+
// So we add the negative after the prefix
687+
classes = [
688+
...classes,
689+
...classes.map(
690+
(cls) => cls.slice(0, prefixLength) + '-' + cls.slice(prefixLength)
691+
),
692+
]
680693
}
681694

682695
return classes

src/util/prefixSelector.js

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import parser from 'postcss-selector-parser'
2-
import { tap } from './tap'
32

4-
export default function (prefix, selector) {
3+
export default function (prefix, selector, prependNegative = false) {
54
return parser((selectors) => {
65
selectors.walkClasses((classSelector) => {
7-
tap(classSelector.value, (baseClass) => {
8-
classSelector.value = `${prefix}${baseClass}`
9-
})
6+
let baseClass = classSelector.value
7+
let shouldPlaceNegativeBeforePrefix = prependNegative && baseClass.startsWith('-')
8+
9+
classSelector.value = shouldPlaceNegativeBeforePrefix
10+
? `-${prefix}${baseClass.slice(1)}`
11+
: `${prefix}${baseClass}`
1012
})
1113
}).processSync(selector)
1214
}

tests/getClassList.test.js

+47-12
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,57 @@ it('should generate every possible class, without variants', () => {
55
let config = {}
66

77
let context = createContext(resolveConfig(config))
8-
expect(context.getClassList()).toBeInstanceOf(Array)
8+
let classes = context.getClassList()
9+
expect(classes).toBeInstanceOf(Array)
910

1011
// Verify we have a `container` for the 'components' section.
11-
expect(context.getClassList()).toContain('container')
12+
expect(classes).toContain('container')
1213

1314
// Verify we handle the DEFAULT case correctly
14-
expect(context.getClassList()).toContain('border')
15+
expect(classes).toContain('border')
1516

1617
// Verify we handle negative values correctly
17-
expect(context.getClassList()).toContain('-inset-1/4')
18-
expect(context.getClassList()).toContain('-m-0')
19-
expect(context.getClassList()).not.toContain('-uppercase')
20-
expect(context.getClassList()).not.toContain('-opacity-50')
21-
expect(
22-
createContext(
23-
resolveConfig({ theme: { extend: { margin: { DEFAULT: '5px' } } } })
24-
).getClassList()
25-
).not.toContain('-m-DEFAULT')
18+
expect(classes).toContain('-inset-1/4')
19+
expect(classes).toContain('-m-0')
20+
expect(classes).not.toContain('-uppercase')
21+
expect(classes).not.toContain('-opacity-50')
22+
23+
config = { theme: { extend: { margin: { DEFAULT: '5px' } } } }
24+
context = createContext(resolveConfig(config))
25+
classes = context.getClassList()
26+
27+
expect(classes).not.toContain('-m-DEFAULT')
28+
})
29+
30+
it('should generate every possible class while handling negatives and prefixes', () => {
31+
let config = { prefix: 'tw-' }
32+
let context = createContext(resolveConfig(config))
33+
let classes = context.getClassList()
34+
expect(classes).toBeInstanceOf(Array)
35+
36+
// Verify we have a `container` for the 'components' section.
37+
expect(classes).toContain('tw-container')
38+
39+
// Verify we handle the DEFAULT case correctly
40+
expect(classes).toContain('tw-border')
41+
42+
// Verify we handle negative values correctly
43+
expect(classes).toContain('-tw-inset-1/4')
44+
expect(classes).toContain('-tw-m-0')
45+
expect(classes).not.toContain('-tw-uppercase')
46+
expect(classes).not.toContain('-tw-opacity-50')
47+
48+
// These utilities do work but there's no reason to generate
49+
// them alongside the `-{prefix}-{utility}` versions
50+
expect(classes).not.toContain('tw--inset-1/4')
51+
expect(classes).not.toContain('tw--m-0')
52+
53+
config = {
54+
prefix: 'tw-',
55+
theme: { extend: { margin: { DEFAULT: '5px' } } },
56+
}
57+
context = createContext(resolveConfig(config))
58+
classes = context.getClassList()
59+
60+
expect(classes).not.toContain('-tw-m-DEFAULT')
2661
})

0 commit comments

Comments
 (0)