mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
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
This commit is contained in:
parent
ab9fd951dd
commit
01fbe196c4
@ -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
|
||||
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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')
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { run, css } from './util/run'
|
||||
import { run, html, css } from './util/run'
|
||||
|
||||
test('prefix', () => {
|
||||
let config = {
|
||||
@ -73,3 +73,288 @@ test('prefix', () => {
|
||||
expect(result.css).toMatchFormattedCss(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('negative values: marker before prefix', async () => {
|
||||
let config = {
|
||||
prefix: 'tw-',
|
||||
content: [{ raw: html`<div class="-tw-top-1"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
await run(input, config)
|
||||
|
||||
const result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.-tw-top-1 {
|
||||
top: -0.25rem;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('negative values: marker after prefix', async () => {
|
||||
let config = {
|
||||
prefix: 'tw-',
|
||||
content: [{ raw: html`<div class="tw--top-1"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
await run(input, config)
|
||||
|
||||
const result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.tw--top-1 {
|
||||
top: -0.25rem;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('negative values: marker before prefix and arbitrary value', async () => {
|
||||
let config = {
|
||||
prefix: 'tw-',
|
||||
content: [{ raw: html`<div class="-tw-top-[1px]"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
await run(input, config)
|
||||
|
||||
const result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.-tw-top-\[1px\] {
|
||||
top: -1px;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('negative values: marker after prefix and arbitrary value', async () => {
|
||||
let config = {
|
||||
prefix: 'tw-',
|
||||
content: [{ raw: html`<div class="tw--top-[1px]"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
await run(input, config)
|
||||
|
||||
const result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.tw--top-\[1px\] {
|
||||
top: -1px;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('negative values: no marker and arbitrary value', async () => {
|
||||
let config = {
|
||||
prefix: 'tw-',
|
||||
content: [{ raw: html`<div class="tw-top-[-1px]"></div>` }],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
await run(input, config)
|
||||
|
||||
const result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.tw-top-\[-1px\] {
|
||||
top: -1px;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('negative values: variant versions', async () => {
|
||||
let config = {
|
||||
prefix: 'tw-',
|
||||
content: [
|
||||
{
|
||||
raw: html`
|
||||
<div class="hover:-tw-top-1 hover:tw--top-1"></div>
|
||||
<div class="hover:-tw-top-[1px] hover:tw--top-[1px]"></div>
|
||||
<div class="hover:tw-top-[-1px]"></div>
|
||||
|
||||
<!-- this one should not generate anything -->
|
||||
<div class="-hover:tw-top-1"></div>
|
||||
`,
|
||||
},
|
||||
],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
await run(input, config)
|
||||
|
||||
const result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.hover\:-tw-top-1:hover {
|
||||
top: -0.25rem;
|
||||
}
|
||||
.hover\:tw--top-1:hover {
|
||||
top: -0.25rem;
|
||||
}
|
||||
.hover\:-tw-top-\[1px\]:hover {
|
||||
top: -1px;
|
||||
}
|
||||
.hover\:tw--top-\[1px\]:hover {
|
||||
top: -1px;
|
||||
}
|
||||
.hover\:tw-top-\[-1px\]:hover {
|
||||
top: -1px;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('negative values: prefix and apply', async () => {
|
||||
let config = {
|
||||
prefix: 'tw-',
|
||||
content: [{ raw: html`` }],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
|
||||
.a {
|
||||
@apply hover:tw--top-1;
|
||||
}
|
||||
.b {
|
||||
@apply hover:-tw-top-1;
|
||||
}
|
||||
.c {
|
||||
@apply hover:-tw-top-[1px];
|
||||
}
|
||||
.d {
|
||||
@apply hover:tw--top-[1px];
|
||||
}
|
||||
.e {
|
||||
@apply hover:tw-top-[-1px];
|
||||
}
|
||||
`
|
||||
|
||||
await run(input, config)
|
||||
|
||||
const result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.a:hover {
|
||||
top: -0.25rem;
|
||||
}
|
||||
.b:hover {
|
||||
top: -0.25rem;
|
||||
}
|
||||
.c:hover {
|
||||
top: -1px;
|
||||
}
|
||||
.d:hover {
|
||||
top: -1px;
|
||||
}
|
||||
.e:hover {
|
||||
top: -1px;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('negative values: prefix in the safelist', async () => {
|
||||
let config = {
|
||||
prefix: 'tw-',
|
||||
safelist: [{ pattern: /-tw-top-1/g }, { pattern: /tw--top-1/g }],
|
||||
theme: {
|
||||
inset: {
|
||||
1: '0.25rem',
|
||||
},
|
||||
},
|
||||
content: [{ raw: html`` }],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
await run(input, config)
|
||||
|
||||
const result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.-tw-top-1 {
|
||||
top: -0.25rem;
|
||||
}
|
||||
.tw--top-1 {
|
||||
top: -0.25rem;
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
it('prefix with negative values and variants in the safelist', async () => {
|
||||
let config = {
|
||||
prefix: 'tw-',
|
||||
safelist: [
|
||||
{ pattern: /-tw-top-1/, variants: ['hover', 'sm:hover'] },
|
||||
{ pattern: /tw--top-1/, variants: ['hover', 'sm:hover'] },
|
||||
],
|
||||
theme: {
|
||||
inset: {
|
||||
1: '0.25rem',
|
||||
},
|
||||
},
|
||||
content: [{ raw: html`` }],
|
||||
corePlugins: { preflight: false },
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
await run(input, config)
|
||||
|
||||
const result = await run(input, config)
|
||||
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.-tw-top-1 {
|
||||
top: -0.25rem;
|
||||
}
|
||||
.tw--top-1 {
|
||||
top: -0.25rem;
|
||||
}
|
||||
.hover\:-tw-top-1:hover {
|
||||
top: -0.25rem;
|
||||
}
|
||||
|
||||
.hover\:tw--top-1:hover {
|
||||
top: -0.25rem;
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.sm\:hover\:-tw-top-1:hover {
|
||||
top: -0.25rem;
|
||||
}
|
||||
.sm\:hover\:tw--top-1:hover {
|
||||
top: -0.25rem;
|
||||
}
|
||||
}
|
||||
`)
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user