diff --git a/CHANGELOG.md b/CHANGELOG.md index 6586568c91b3..74ba883757a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts index 2f29c130fb67..411f0eb9ee3a 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.test.ts @@ -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, diff --git a/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts index 065f10807326..44aa2675db83 100644 --- a/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts +++ b/packages/@tailwindcss-upgrade/src/template/codemods/arbitrary-value-to-bare-value.ts @@ -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 (