Ignore --tw- variables during internal signature computation (#19156)

This PR improves some of the signature computation logic. Right now,
when you want to convert `[font-weight:400]` to `font-normal` it's not
going to work because the signatures don't like up:

```css
/* [font-weight:400] */
.x {
  font-weight: 400;
}

/* font-normal */
.x {
  --tw-font-weight: 400;
  font-weight: 400;
}
```

So this PR essentially ignores `--tw-{property}` _if_ the `{property}`
exists with the exact same value.

The main reason we have this, is to make composition of utilities
easier.

As with any of these upgrades, they are the expected behavior in most
cases, but there could always be a case where you don't want this, but
that's why the upgrade tool requires you to review each change before
applying it. Intellisense will recommend the simplified class, but it's
up to you to decide if you want to apply it or not.

There is a known edge case for `leading-{num}`, because the property is
`line-height` and the CSS variable is `--tw-leading`. Right now we map
`--tw-leading` to `line-height` but we can also update the leading
classes to use `--tw-line-height` instead but that feels like a bigger
breaking change _if_ people rely on these internal CSS variables...
This commit is contained in:
Robin Malfait 2025-10-20 17:21:07 +02:00 committed by GitHub
parent 66c18ca8a4
commit 7537e34fd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 25 additions and 15 deletions

View File

@ -167,7 +167,7 @@ test(
"
--- src/index.html ---
<div
class="[letter-spacing:var(--tracking-super-wide)] [line-height:var(--leading-super-loose)]"
class="tracking-super-wide leading-super-loose"
></div>
<div class="text-super-red/super-opaque leading-super-loose"></div>

View File

@ -253,6 +253,14 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50',
],
// If a utility sets `property` and `--tw-{property}` with the same value,
// we can ignore the `--tw-{property}`. This is just here for composition.
// This means that we should be able to upgrade the one _without_ to the one
// _with_ the variable
['[font-weight:400]', 'font-normal'],
['[line-height:0]', 'leading-0'],
['[border-style:solid]', 'border-solid'],
])(testName, async (candidate, expected) => {
await expectCanonicalization(
css`

View File

@ -11,8 +11,6 @@ import { isValidSpacingMultiplier } from './utils/infer-data-type'
import * as ValueParser from './value-parser'
import { walk, WalkAction } from './walk'
const FLOATING_POINT_PERCENTAGE = /\d*\.\d+(?:[eE][+-]?\d+)?%/g
export enum SignatureFeatures {
None = 0,
ExpandProperties = 1 << 0,
@ -101,29 +99,33 @@ function canonicalizeAst(ast: AstNode[], options: SignatureOptions) {
let { rem, designSystem } = options
walk(ast, {
enter(node) {
enter(node, ctx) {
// Optimize declarations
if (node.kind === 'declaration') {
if (node.value === undefined || node.property === '--tw-sort') {
return WalkAction.Replace([])
}
// Ignore `--tw-{property}` if `{property}` exists with the same value
if (node.property.startsWith('--tw-')) {
if (
(ctx.parent?.nodes ?? []).some(
(sibling) =>
sibling.kind === 'declaration' &&
node.value === sibling.value &&
node.important === sibling.important &&
!sibling.property.startsWith('--tw-'),
)
) {
return WalkAction.Replace([])
}
}
if (options.features & SignatureFeatures.ExpandProperties) {
let replacement = expandDeclaration(node, options.features)
if (replacement) return WalkAction.Replace(replacement)
}
// Normalize percentages by removing unnecessary dots and zeros.
//
// E.g.: `50.0%` → `50%`
if (node.value.includes('%')) {
FLOATING_POINT_PERCENTAGE.lastIndex = 0
node.value = node.value.replaceAll(
FLOATING_POINT_PERCENTAGE,
(match) => `${Number(match.slice(0, -1))}%`,
)
}
// Resolve theme values to their inlined value.
if (node.value.includes('var(')) {
node.value = resolveVariablesInValue(node.value, designSystem)