From a80b6991e5d576198c469d2f139a29e4f1d8c2c1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 12:59:09 +0200 Subject: [PATCH 1/8] start of feat/canonicalize-ignore-tw-vars From 113a42abc5b1ba298d3d58033815b500c3e6d070 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 12:42:00 +0200 Subject: [PATCH 2/8] add failing tests --- packages/tailwindcss/src/canonicalize-candidates.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/tailwindcss/src/canonicalize-candidates.test.ts b/packages/tailwindcss/src/canonicalize-candidates.test.ts index 9eac5cb92d40..60c23c22106c 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` From 54dd8cbebbd4f1274f137df137aed7ef55a6e8ff Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 12:27:47 +0200 Subject: [PATCH 3/8] remove unnecessary percentage handling We already constant fold declarations, including normalization of `0.00%` to just `0%`. So we don't need this code anymore. --- packages/tailwindcss/src/signatures.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/tailwindcss/src/signatures.ts b/packages/tailwindcss/src/signatures.ts index 3e6a6605d381..dbbb6f44fd22 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, @@ -113,17 +111,6 @@ function canonicalizeAst(ast: AstNode[], options: SignatureOptions) { 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) From 10177a05af02beb381e6aae6f8c3556254fdb31b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 12:34:52 +0200 Subject: [PATCH 4/8] ignore `--tw-{property}` when `{property}` exists with the same value This will help with `[font-weight:400]` to `font-normal`, even though `font-normal` is actually implemented as: ```css .font-normal { --tw-font-weight: 400; font-weight: 400; } ``` --- packages/tailwindcss/src/signatures.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/signatures.ts b/packages/tailwindcss/src/signatures.ts index dbbb6f44fd22..8b1f87ec4647 100644 --- a/packages/tailwindcss/src/signatures.ts +++ b/packages/tailwindcss/src/signatures.ts @@ -99,13 +99,32 @@ 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-')) { + let siblings = ctx.parent?.nodes + if (siblings) { + let other = node.property.slice(5) + if ( + siblings.some( + (sibling) => + sibling.kind === 'declaration' && + sibling.property === other && + node.value === sibling.value && + node.important === sibling.important, + ) + ) { + return WalkAction.Replace([]) + } + } + } + if (options.features & SignatureFeatures.ExpandProperties) { let replacement = expandDeclaration(node, options.features) if (replacement) return WalkAction.Replace(replacement) From 6991ff0866205ae823ec4423e4e39c568a93883b Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 12:40:30 +0200 Subject: [PATCH 5/8] add known edge case Not super keen on this exception here, but it's something we do. Another solution here is to use `--tw-line-height` instead so it properly maps to the other property. --- packages/tailwindcss/src/signatures.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/tailwindcss/src/signatures.ts b/packages/tailwindcss/src/signatures.ts index 8b1f87ec4647..5eedc18f4df3 100644 --- a/packages/tailwindcss/src/signatures.ts +++ b/packages/tailwindcss/src/signatures.ts @@ -111,6 +111,12 @@ function canonicalizeAst(ast: AstNode[], options: SignatureOptions) { let siblings = ctx.parent?.nodes if (siblings) { let other = node.property.slice(5) + + // Edge cases: + // + // `leading-*` sets `--tw-leading` but the property is `line-height` + if (node.property === '--tw-leading') other = 'line-height' + if ( siblings.some( (sibling) => From 5737ef753912ab16977a82d0676e177fcdb628aa Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 14:56:10 +0200 Subject: [PATCH 6/8] map `--tw-tracking` to `letter-spacing` --- packages/tailwindcss/src/signatures.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/tailwindcss/src/signatures.ts b/packages/tailwindcss/src/signatures.ts index 5eedc18f4df3..1e88af4b3e90 100644 --- a/packages/tailwindcss/src/signatures.ts +++ b/packages/tailwindcss/src/signatures.ts @@ -116,6 +116,7 @@ function canonicalizeAst(ast: AstNode[], options: SignatureOptions) { // // `leading-*` sets `--tw-leading` but the property is `line-height` if (node.property === '--tw-leading') other = 'line-height' + if (node.property === '--tw-tracking') other = 'letter-spacing' if ( siblings.some( From e1a637505ca6d8315f1e511c5c44bc89ae3029dd Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 14:58:19 +0200 Subject: [PATCH 7/8] update integration tests --- integrations/upgrade/js-config.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/upgrade/js-config.test.ts b/integrations/upgrade/js-config.test.ts index de10250c29a1..039f3dc8dd57 100644 --- a/integrations/upgrade/js-config.test.ts +++ b/integrations/upgrade/js-config.test.ts @@ -167,7 +167,7 @@ test( " --- src/index.html ---
From dc60f7aad0d9587260faf44d8cb6cf615faa9ae5 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 20 Oct 2025 16:05:00 +0200 Subject: [PATCH 8/8] drop any `--tw-*` if another property with the same value exists This way we don't have to hardcode the edge cases, but we have to make sure that the same value exists _somewhere_. We also make sure that the other property is also not a CSS variable. Otherwise we would run into: ```css .foo { --tw-a: 1; --tw-b: 1; --tw-c: 1; } ``` Because this would delete all the declarations --- packages/tailwindcss/src/signatures.ts | 31 +++++++++----------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/packages/tailwindcss/src/signatures.ts b/packages/tailwindcss/src/signatures.ts index 1e88af4b3e90..7ad608f22cb4 100644 --- a/packages/tailwindcss/src/signatures.ts +++ b/packages/tailwindcss/src/signatures.ts @@ -108,27 +108,16 @@ function canonicalizeAst(ast: AstNode[], options: SignatureOptions) { // Ignore `--tw-{property}` if `{property}` exists with the same value if (node.property.startsWith('--tw-')) { - let siblings = ctx.parent?.nodes - if (siblings) { - let other = node.property.slice(5) - - // Edge cases: - // - // `leading-*` sets `--tw-leading` but the property is `line-height` - if (node.property === '--tw-leading') other = 'line-height' - if (node.property === '--tw-tracking') other = 'letter-spacing' - - if ( - siblings.some( - (sibling) => - sibling.kind === 'declaration' && - sibling.property === other && - node.value === sibling.value && - node.important === sibling.important, - ) - ) { - return WalkAction.Replace([]) - } + if ( + (ctx.parent?.nodes ?? []).some( + (sibling) => + sibling.kind === 'declaration' && + node.value === sibling.value && + node.important === sibling.important && + !sibling.property.startsWith('--tw-'), + ) + ) { + return WalkAction.Replace([]) } }