Skip to content

Commit

Permalink
Fix negative utility generation and detection when using a prefix (#7295
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
thecrypticace committed Feb 7, 2022
1 parent ab9fd95 commit 01fbe19
Show file tree
Hide file tree
Showing 6 changed files with 384 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Correctly parse shadow lengths without a leading zero ([#7289](https://github.com/tailwindlabs/tailwindcss/pull/7289))
- Don't crash when scanning extremely long class candidates ([#7331](https://github.com/tailwindlabs/tailwindcss/pull/7331))
- Use less hacky fix for urls detected as custom properties ([#7275](https://github.com/tailwindlabs/tailwindcss/pull/7275))
- Correctly generate negative utilities when dash is before the prefix ([#7295](https://github.com/tailwindlabs/tailwindcss/pull/7295))
- Detect prefixed negative utilities in the safelist ([#7295](https://github.com/tailwindlabs/tailwindcss/pull/7295))

## [3.0.18] - 2022-01-28

Expand Down
28 changes: 26 additions & 2 deletions src/lib/generateRules.js
Expand Up @@ -63,9 +63,23 @@ function applyPrefix(matches, context) {
let [meta] = match
if (meta.options.respectPrefix) {
let container = postcss.root({ nodes: [match[1].clone()] })
let classCandidate = match[1].raws.tailwind.classCandidate

container.walkRules((r) => {
r.selector = prefixSelector(context.tailwindConfig.prefix, r.selector)
// If this is a negative utility with a dash *before* the prefix we
// have to ensure that the generated selector matches the candidate

// Not doing this will cause `-tw-top-1` to generate the class `.tw--top-1`
// The disconnect between candidate <-> class can cause @apply to hard crash.
let shouldPrependNegative = classCandidate.startsWith('-')

r.selector = prefixSelector(
context.tailwindConfig.prefix,
r.selector,
shouldPrependNegative
)
})

match[1] = container.nodes[0]
}
}
Expand Down Expand Up @@ -371,6 +385,14 @@ function splitWithSeparator(input, separator) {
return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g'))
}

function* recordCandidates(matches, classCandidate) {
for (const match of matches) {
match[1].raws.tailwind = { classCandidate }

yield match
}
}

function* resolveMatches(candidate, context) {
let separator = context.tailwindConfig.separator
let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
Expand Down Expand Up @@ -482,7 +504,9 @@ function* resolveMatches(candidate, context) {
continue
}

matches = applyPrefix(matches.flat(), context)
matches = matches.flat()
matches = Array.from(recordCandidates(matches, classCandidate))
matches = applyPrefix(matches, context)

if (important) {
matches = applyImportant(matches, context)
Expand Down
19 changes: 16 additions & 3 deletions src/lib/setupContextUtils.js
Expand Up @@ -666,17 +666,30 @@ function registerPlugins(plugins, context) {

if (checks.length > 0) {
let patternMatchingCount = new Map()
let prefixLength = context.tailwindConfig.prefix.length

for (let util of classList) {
let utils = Array.isArray(util)
? (() => {
let [utilName, options] = util
let classes = Object.keys(options?.values ?? {}).map((value) =>
formatClass(utilName, value)
)
let values = Object.keys(options?.values ?? {})
let classes = values.map((value) => formatClass(utilName, value))

if (options?.supportsNegativeValues) {
// This is the normal negated version
// e.g. `-inset-1` or `-tw-inset-1`
classes = [...classes, ...classes.map((cls) => '-' + cls)]

// This is the negated version *after* the prefix
// e.g. `tw--inset-1`
// The prefix is already attached to util name
// So we add the negative after the prefix
classes = [
...classes,
...classes.map(
(cls) => cls.slice(0, prefixLength) + '-' + cls.slice(prefixLength)
),
]
}

return classes
Expand Down
12 changes: 7 additions & 5 deletions src/util/prefixSelector.js
@@ -1,12 +1,14 @@
import parser from 'postcss-selector-parser'
import { tap } from './tap'

export default function (prefix, selector) {
export default function (prefix, selector, prependNegative = false) {
return parser((selectors) => {
selectors.walkClasses((classSelector) => {
tap(classSelector.value, (baseClass) => {
classSelector.value = `${prefix}${baseClass}`
})
let baseClass = classSelector.value
let shouldPlaceNegativeBeforePrefix = prependNegative && baseClass.startsWith('-')

classSelector.value = shouldPlaceNegativeBeforePrefix
? `-${prefix}${baseClass.slice(1)}`
: `${prefix}${baseClass}`
})
}).processSync(selector)
}
59 changes: 47 additions & 12 deletions tests/getClassList.test.js
Expand Up @@ -5,22 +5,57 @@ it('should generate every possible class, without variants', () => {
let config = {}

let context = createContext(resolveConfig(config))
expect(context.getClassList()).toBeInstanceOf(Array)
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)

// Verify we have a `container` for the 'components' section.
expect(context.getClassList()).toContain('container')
expect(classes).toContain('container')

// Verify we handle the DEFAULT case correctly
expect(context.getClassList()).toContain('border')
expect(classes).toContain('border')

// Verify we handle negative values correctly
expect(context.getClassList()).toContain('-inset-1/4')
expect(context.getClassList()).toContain('-m-0')
expect(context.getClassList()).not.toContain('-uppercase')
expect(context.getClassList()).not.toContain('-opacity-50')
expect(
createContext(
resolveConfig({ theme: { extend: { margin: { DEFAULT: '5px' } } } })
).getClassList()
).not.toContain('-m-DEFAULT')
expect(classes).toContain('-inset-1/4')
expect(classes).toContain('-m-0')
expect(classes).not.toContain('-uppercase')
expect(classes).not.toContain('-opacity-50')

config = { theme: { extend: { margin: { DEFAULT: '5px' } } } }
context = createContext(resolveConfig(config))
classes = context.getClassList()

expect(classes).not.toContain('-m-DEFAULT')
})

it('should generate every possible class while handling negatives and prefixes', () => {
let config = { prefix: 'tw-' }
let context = createContext(resolveConfig(config))
let classes = context.getClassList()
expect(classes).toBeInstanceOf(Array)

// Verify we have a `container` for the 'components' section.
expect(classes).toContain('tw-container')

// Verify we handle the DEFAULT case correctly
expect(classes).toContain('tw-border')

// Verify we handle negative values correctly
expect(classes).toContain('-tw-inset-1/4')
expect(classes).toContain('-tw-m-0')
expect(classes).not.toContain('-tw-uppercase')
expect(classes).not.toContain('-tw-opacity-50')

// These utilities do work but there's no reason to generate
// them alongside the `-{prefix}-{utility}` versions
expect(classes).not.toContain('tw--inset-1/4')
expect(classes).not.toContain('tw--m-0')

config = {
prefix: 'tw-',
theme: { extend: { margin: { DEFAULT: '5px' } } },
}
context = createContext(resolveConfig(config))
classes = context.getClassList()

expect(classes).not.toContain('-tw-m-DEFAULT')
})

0 comments on commit 01fbe19

Please sign in to comment.