From 7537e34fd17c8dda04113fd7987f39ae57a158f4 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 17:21:07 +0200 Subject: [PATCH] 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... --- integrations/upgrade/js-config.test.ts | 2 +- .../src/canonicalize-candidates.test.ts | 8 +++++ packages/tailwindcss/src/signatures.ts | 30 ++++++++++--------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index de10250c2..039f3dc8d 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -167,7 +167,7 @@ test( " --- src/index.html ---
diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 9eac5cb92..60c23c221 100644 --- a/packages/tailwindcss/src/canonicalize-candidates.test.ts +++ b/packages/tailwindcss/src/canonicalize-candidates.test.ts @@ -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` diff --git a/packages/tailwindcss/src/signatures.ts b/packages/tailwindcss/src/signatures.ts index 3e6a6605d..7ad608f22 100644 --- a/packages/tailwindcss/src/signatures.ts +++ b/packages/tailwindcss/src/signatures.ts @@ -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)