Migrate arbitrary values to bare values (#14669)

This PR adds a codemod that can convert arbitrary values to the cleaner
bare values if we can.

For example, some classes use arbitrary values such as `col-start-[16]`,
but in v4 we have bare values for some plugins that don't really need to
adhere to your design system.

In this case, we can convert `col-start-[16]` to just `col-start-16`.

Another use case is for utilities that use fractions. For example the
`aspect-*` plugin.

A custom aspect ratio such as `aspect-[16/9]` can be converted to
`aspect-16/9`.

There are some rules attached to this migration:

1. We can only migrate arbitrary values that is a single positive
integer, or an arbitrary value that is a fraction where the numerator
and denominator are both positive integers.
2. We make sure that some CSS can be generated once its converted to a
bare value.
This commit is contained in:
Robin Malfait 2024-10-15 11:45:03 +02:00 • committed by GitHub
parent 36acad0306
commit 2b13547a0d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 92 additions and 0 deletions

View file

@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603))
- _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635))
- _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644))
- _Upgrade (experimental)_: Migrate arbitrary values to bare values ([#14669](https://github.com/tailwindlabs/tailwindcss/pull/14669))
### Fixed

View file

@ -3,6 +3,24 @@ import { expect, test } from 'vitest'
import { arbitraryValueToBareValue } from './arbitrary-value-to-bare-value'
test.each([
['aspect-[12/34]', 'aspect-12/34'],
['aspect-[1.2/34]', 'aspect-[1.2/34]'],
['col-start-[7]', 'col-start-7'],
['flex-[2]', 'flex-2'], // `flex` is implemented as static and functional utilities
// Only 50-200% (inclusive) are valid:
// https://developer.mozilla.org/en-US/docs/Web/CSS/font-stretch#percentage
['font-stretch-[50%]', 'font-stretch-50%'],
['font-stretch-[50.5%]', 'font-stretch-[50.5%]'],
['font-stretch-[201%]', 'font-stretch-[201%]'],
['font-stretch-[49%]', 'font-stretch-[49%]'],
// Should stay as-is
['font-stretch-[1/2]', 'font-stretch-[1/2]'],
// This test in itself is a bit flawed because `text-[1/2]` currently
// generates something. Converting it to `text-1/2` doesn't produce anything.
['text-[1/2]', 'text-[1/2]'],
['data-[selected]:flex', 'data-selected:flex'],
['data-[foo=bar]:flex', 'data-[foo=bar]:flex'],
@ -22,6 +40,10 @@ test.each([
['group-has-aria-[selected]:flex', 'group-has-aria-[selected]:flex'],
['max-lg:hover:data-[selected]:flex!', 'max-lg:hover:data-selected:flex!'],
[
'data-[selected]:aria-[selected="true"]:aspect-[12/34]',
'data-selected:aria-selected:aspect-12/34',
],
])('%s => %s', async (candidate, result) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,

View file

@ -1,6 +1,7 @@
import type { Config } from 'tailwindcss'
import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { isPositiveInteger } from '../../../../tailwindcss/src/utils/infer-data-type'
import { segment } from '../../../../tailwindcss/src/utils/segment'
import { printCandidate } from '../candidates'
@ -12,6 +13,74 @@ export function arbitraryValueToBareValue(
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
let clone = structuredClone(candidate)
let changed = false
// Convert font-stretch-* utilities
if (
clone.kind === 'functional' &&
clone.value?.kind === 'arbitrary' &&
clone.value.dataType === null &&
clone.root === 'font-stretch'
) {
if (clone.value.value.endsWith('%') && isPositiveInteger(clone.value.value.slice(0, -1))) {
let percentage = parseInt(clone.value.value)
if (percentage >= 50 && percentage <= 200) {
changed = true
clone.value = {
kind: 'named',
value: clone.value.value,
fraction: null,
}
}
}
}
// Convert arbitrary values with positive integers to bare values
// Convert arbitrary values with fractions to bare values
else if (
clone.kind === 'functional' &&
clone.value?.kind === 'arbitrary' &&
clone.value.dataType === null
) {
let parts = segment(clone.value.value, '/')
if (parts.every((part) => isPositiveInteger(part))) {
changed = true
let currentValue = clone.value
let currentModifier = clone.modifier
// E.g.: `col-start-[12]`
// ^^
if (parts.length === 1) {
clone.value = {
kind: 'named',
value: clone.value.value,
fraction: null,
}
}
// E.g.: `aspect-[12/34]`
// ^^ ^^
else {
clone.value = {
kind: 'named',
value: parts[0],
fraction: clone.value.value,
}
clone.modifier = {
kind: 'named',
value: parts[1],
}
}
// Double check that the new value compiles correctly
if (designSystem.compileAstNodes(clone).length === 0) {
clone.value = currentValue
clone.modifier = currentModifier
changed = false
}
}
}
for (let variant of variants(clone)) {
// Convert `data-[selected]` to `data-selected`
if (