Improve canonicalization for bare values exceeding default spacing scale suggestions (#19809)

This PR adds support for canonicalization of utilities that accept bare
values and exceed the default spacing scale we use for intellisense.

Right now, all utilities are behind functions, so the only way to know
whether something compiles is by compiling a candidate, e.g. `w-8` and
passing it to the utility functions. To help us, we use the intellisense
APIs that we use for suggestions.

Most utilities that accept bare values, have suggestions up until
`*-96`, so `w-96 h-96` would be canonicalized to `size-96`. But the
moment we exceed that, the result stays as-is.
```
→ w-96 h-96
= size-96

→ w-1234 h-1234
= h-1234 w-1234
```

This PR ensures that the last scenario also gets canonicalized to
`size-1234` instead of staying as `h-1234 w-1234`.

```
→ w-96 h-96
= size-96

→ w-1234 h-1234
= size-1234
```

## Test plan

1. Existing tests pass
2. Added new tests for utilities with bare values

[ci-all] just to see if this additional logic doesn't cause timeouts in
CI for WIndows. In my testing this doesn't have a significant impact on
performance at all.
This commit is contained in:
Robin Malfait 2026-03-16 22:44:29 +01:00 • committed by GitHub
parent aaaefe8b5d
commit bb2f170514
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 55 additions and 1 deletions

View file

@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fix crash in canonicalization step when handling utilities with empty property maps ([#19727](https://github.com/tailwindlabs/tailwindcss/pull/19727))
- Skip full reload for server only modules scanned by client CSS when using `@tailwindcss/vite` ([#19745](https://github.com/tailwindlabs/tailwindcss/pull/19745))
- Add support for Vite 8 in `@tailwindcss/vite` ([#19790](https://github.com/tailwindlabs/tailwindcss/pull/19790))
- Improve canonicalization for bare values exceeding default spacing scale suggestions (e.g. `w-1234 h-1234` → `size-1234`) ([#19809](https://github.com/tailwindlabs/tailwindcss/pull/19809))
## [4.2.1] - 2026-02-23

View file

@ -1054,6 +1054,13 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
// To completely different utility
['w-4 h-4', 'size-4'],
// Goes beyond the default spacing scale that's being used in intellisense
// for code completion. Since it's about bare values, we should still be
// able to combine them.
['w-123 h-123', 'size-123'],
['w-128 h-128', 'size-128'], // `w-128` on its own would become `w-lg`
['mt-123 mb-123', 'my-123'],
// Do not touch if not operating on the same variants
['hover:w-4 h-4', 'hover:w-4 h-4'],

View file

@ -312,12 +312,52 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
}
}
let dynamicUtilities = new DefaultMap((candidate: string) => {
let result = new DefaultMap(
(_property: string) => new DefaultMap((_value: string) => new Set<string>()),
)
let relevantProperties = new Set(computeUtilitiesPropertiesLookup.get(candidate).keys())
if (relevantProperties.size === 0) return result
for (let parsedCandidate of parseCandidate(designSystem, candidate)) {
if (
parsedCandidate.kind !== 'functional' ||
parsedCandidate.value?.kind !== 'named' // Necessary for bare values
) {
continue
}
for (let root of designSystem.utilities.keys('functional')) {
if (root === parsedCandidate.root) continue // Skip self
let replacement = printUnprefixedCandidate(designSystem, {
...cloneCandidate(parsedCandidate),
root,
})
let propertyValues = computeUtilitiesPropertiesLookup.get(replacement)
for (let [property, values] of propertyValues) {
if (!relevantProperties.has(property)) continue // Skip properties that are not relevant for the current candidate
for (let value of values) {
result.get(property).get(value).add(replacement)
}
}
}
return result
}
return result
})
// For each property, lookup other utilities that also set this property and
// this exact value. If multiple properties are used, use the intersection of
// each property.
//
// E.g.: `margin-top` → `mt-1`, `my-1`, `m-1`
let otherUtilities = candidatePropertiesValues.map((propertyValues) => {
let otherUtilities = candidatePropertiesValues.map((propertyValues, idx) => {
let result: Set<string> | null = null
for (let property of propertyValues.keys()) {
let otherUtilities = new Set<string>()
@ -327,6 +367,12 @@ function collapseCandidates(options: InternalCanonicalizeOptions, candidates: st
}
}
for (let value of propertyValues.get(property)) {
for (let candidate of dynamicUtilities.get(candidates[idx]).get(property).get(value)) {
otherUtilities.add(candidate)
}
}
if (result === null) result = otherUtilities
else result = intersection(result, otherUtilities)