Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix negative utility generation and detection when using a prefix #7295

Merged
merged 7 commits into from Feb 7, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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')
})