Canonicalization: prevent inlining CSS-wide keywords (#20417)
This PR fixes an issue where canonicalization suggestions in
intellisense result in 'weird' suggestions.
```
The class text-foreground/60 can be written as text-default-soft-hover
```
If we look at the CSS provided by the issue, this doesn't immediately
make sense:
```css
@theme {
--color-foreground: var(--foreground);
--color-default-soft-hover: color-mix(in oklab, var(--default) 60%, transparent);
}
:root {
--foreground: oklch(0.2103 0.0059 285.89); /* near-black */
--default: oklch(94% 0.001 286.375); /* light gray */
}
```
But it turns out that when you use Uniwind (React Native) with HeroUI,
that the setup looks more like this:
```css
@import 'tailwindcss';
@theme {
--foreground: unset;
--default: unset;
}
@theme inline {
--color-foreground: var(--foreground);
--color-default-soft-hover: color-mix(in oklab, var(--default) 60%, transparent);
}
```
During the canonicalization step, we inline all the `@theme` values, the
reason for this is that `text-[#fff]` can be turned into `text-white`
even though they look slightly different:
```css
.text-\[\#fff\] {
color: #fff;
}
.text-white {
color: var(--color-white, #fff);
}
```
But when we inline them, it looks like this:
```css
.text-\[\#fff\] {
color: #fff;
}
.text-white {
color: #fff;
}
```
Internally, we use signatures to make sure that they are safe to be
subtituted with eachother. In this case, the signatures will look like
this:
```css
.x {
color: #fff;
}
.x {
color: #fff;
}
```
If we now look at the signatures of the original issue, you would see:
```css
/* text-foreground/60 */
.x {
color: color-mix(in_oklab,unset_60%,transparent);
}
/* text-default-soft-hover */
.x {
color: color-mix(in_oklab,unset_60%,transparent);
}
```
That's because the `@theme` variables were inlined, resulting in the
exact same signature, thus we consider them the same.
This PR makes sure to never inline [CSS-wide
keywords](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/Data_types#css-wide_keywords)
(such as `unset`) and therefore keeping the CSS variable reference:
```css
/* text-foreground/60 */
.x {
color: color-mix(in_oklab,var(--foreground)_60%,transparent);
}
/* text-default-soft-hover */
.x {
color: color-mix(in_oklab,var(--default)_60%,transparent);
}
```
Fixes:
https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1610
## Test plan
1. Existing tests pass
2. Added a regression test
Checked manually in the regression repo
Before:
<img width="1358" height="421" alt="image"
src="https://github.com/user-attachments/assets/8ab3de97-17e4-4e11-b335-bb37218ff2ee"
/>
After:
<img width="1177" height="247" alt="image"
src="https://github.com/user-attachments/assets/cbbfcf04-3ed3-4fa7-a3eb-ef607239f4ad"
/>
---------
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
parent
f7f58f0806
commit
021b7fe6f2
3 changed files with 53 additions and 8 deletions
|
|
@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Skip ignored directories entirely when computing watch globs (`scanner.globs`), instead of walking their full contents on every rebuild ([#20408](https://github.com/tailwindlabs/tailwindcss/pull/20408))
|
||||
- Oxide: drop invalid UTF-8 candidates ([#20389](https://github.com/tailwindlabs/tailwindcss/pull/20389))
|
||||
- `@tailwindcss/vite` no longer forces a full page reload for external files (e.g.: `.php` files) ([#20414](https://github.com/tailwindlabs/tailwindcss/issues/20414))
|
||||
- Canonicalization: don't merge utilities that reference different theme variables set to CSS-wide keywords like `unset` ([#20417](https://github.com/tailwindlabs/tailwindcss/pull/20417))
|
||||
|
||||
## [4.3.3] - 2026-07-16
|
||||
|
||||
|
|
|
|||
|
|
@ -1638,4 +1638,26 @@ describe('regressions', () => {
|
|||
'lg:flex',
|
||||
])
|
||||
})
|
||||
|
||||
// https://github.com/tailwindlabs/tailwindcss-intellisense/issues/1610
|
||||
test('does not merge utilities whose theme variables resolve to CSS-wide keywords', async () => {
|
||||
let designSystem = await __unstable__loadDesignSystem(
|
||||
css`
|
||||
@tailwind utilities;
|
||||
@theme {
|
||||
--foreground: unset;
|
||||
--default: unset;
|
||||
}
|
||||
@theme inline {
|
||||
--color-foreground: var(--foreground);
|
||||
--color-default-soft-hover: color-mix(in oklab, var(--default) 60%, transparent);
|
||||
}
|
||||
`,
|
||||
{ base: __dirname },
|
||||
)
|
||||
|
||||
expect(
|
||||
designSystem.canonicalizeCandidates(['text-foreground/60', 'text-default-soft-hover']),
|
||||
).toEqual(['text-foreground/60', 'text-default-soft-hover'])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2563,6 +2563,25 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
|
|||
return ast
|
||||
}
|
||||
|
||||
// Variables whose theme value is a CSS-wide keyword (e.g.: `unset`) are never
|
||||
// inlined. These are typically registered as a placeholder to be re-assigned at
|
||||
// runtime, so two variables that share such a value are not interchangeable.
|
||||
//
|
||||
// E.g.:
|
||||
//
|
||||
// ```css
|
||||
// @theme {
|
||||
// --foreground: unset;
|
||||
// --background: unset;
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// Inlining would make `text-(--foreground)` and `text-(--background)` produce
|
||||
// the same signature `color: unset`, even though they are different at runtime.
|
||||
//
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/Data_types#css-wide_keywords
|
||||
const CSS_WIDE_KEYWORDS = ['initial', 'inherit', 'revert', 'revert-layer', 'revert-rule', 'unset']
|
||||
|
||||
// Resolve theme values to their inlined value.
|
||||
//
|
||||
// E.g.:
|
||||
|
|
@ -2578,8 +2597,8 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
|
|||
// }
|
||||
// ```
|
||||
//
|
||||
// Which conveniently will be equivalent to: `text-red-500` when we inline
|
||||
// the value.
|
||||
// Which conveniently will be equivalent to: `text-red-500` when we inline the
|
||||
// value.
|
||||
//
|
||||
// Without inlining:
|
||||
// ```css
|
||||
|
|
@ -2595,13 +2614,13 @@ function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: Si
|
|||
// }
|
||||
// ```
|
||||
//
|
||||
// Recently we made sure that utilities like `text-red-500` also generate
|
||||
// the fallback value for usage in `@reference` mode.
|
||||
// Recently we made sure that utilities like `text-red-500` also generate the
|
||||
// fallback value for usage in `@reference` mode.
|
||||
//
|
||||
// The second assumption is that if you use `var(--key, fallback)` that
|
||||
// happens to match a known variable _and_ its inlined value. Then we can
|
||||
// replace it with the inlined variable. This allows us to handle custom
|
||||
// `@theme` and `@theme inline` definitions.
|
||||
// The second assumption is that if you use `var(--key, fallback)` that happens
|
||||
// to match a known variable _and_ its inlined value. Then we can replace it
|
||||
// with the inlined variable. This allows us to handle custom `@theme` and
|
||||
// `@theme inline` definitions.
|
||||
function resolveVariablesInValue(value: string, designSystem: DesignSystem): string {
|
||||
let changed = false
|
||||
let valueAst = ValueParser.parse(value)
|
||||
|
|
@ -2630,6 +2649,9 @@ function resolveVariablesInValue(value: string, designSystem: DesignSystem): str
|
|||
seen.add(variable)
|
||||
if (variableValue === undefined) return // Couldn't resolve the variable
|
||||
|
||||
// CSS-wide keywords are never inlined
|
||||
if (CSS_WIDE_KEYWORDS.includes(variableValue.toLowerCase())) return
|
||||
|
||||
// Inject variable fallbacks when no fallback is present yet.
|
||||
//
|
||||
// A fallback could consist of multiple values.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue