Add more canonicalization rules for deprecated utilities (#19849)

This PR adds more canonicalization rules for deprecated utilities.

| Before | After |
| --- | --- |
| `overflow-ellipsis` | `text-ellipsis` |
| `start-full` | `inset-s-full` |
| `-start-full` | `-inset-s-full` |
| `start-auto` | `inset-s-auto` |
| `start-px` | `inset-s-px` |
| `-start-px` | `-inset-s-px` |
| `start-8` | `inset-s-8` |
| `-start-8` | `-inset-s-8` |
| `start-123` | `inset-s-123` |
| `-start-123` | `-inset-s-123` |
| `end-full` | `inset-e-full` |
| `-end-full` | `-inset-e-full` |
| `end-auto` | `inset-e-auto` |
| `end-px` | `inset-e-px` |
| `-end-px` | `-inset-e-px` |
| `end-8` | `inset-e-8` |
| `-end-8` | `-inset-e-8` |
| `end-123` | `inset-e-123` |
| `-end-123` | `-inset-e-123` |

In a few cases we already had canonicalization rules, for example
`start-8` where `8` is one of the default suggested spacing scale
values. But this now adds support for positive and negative values that
exceed the default suggested spacing scale as well as some keywords.

## Test plan

1. Existing tests pass
2. Added new tests to ensure these canonicalizations work
This commit is contained in:
Robin Malfait 2026-03-25 11:38:28 +01:00 • committed by GitHub
parent e4856c9720
commit 88a2d22c2f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 87 additions and 64 deletions

View file

@ -27,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Upgrade: Use `config.content` when migrating from Tailwind CSS v3 to Tailwind CSS v4 ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Upgrade: Never migrate files that are ignored by git ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Add `.env` and `.env.*` to default ignored content files ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
- Canonicalization: migrate `overflow-ellipsis` into `text-ellipsis` ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
- Canonicalization: migrate `start-full` → `inset-s-full`, `start-auto` → `inset-s-auto`, `start-px` → `inset-s-px`, and `start-<number>` → `inset-s-<number>` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
- Canonicalization: migrate `end-full` → `inset-e-full`, `end-auto` → `inset-e-auto`, `end-px` → `inset-e-px`, and `end-<number>` → `inset-e-<number>` as well as negative versions ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
## [4.2.2] - 2026-03-18

View file

@ -1,3 +1,4 @@
import { randomUUID } from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, test } from 'vitest'
@ -611,68 +612,67 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
})
describe('deprecated utilities', () => {
test('`order-none` → `order-0`', { timeout }, async () => {
let candidate = 'order-none'
let expected = 'order-0'
let deprecated = [
['order-none', 'order-0'],
['break-words', 'wrap-break-word'],
['overflow-ellipsis', 'text-ellipsis'],
let input = css`
@import 'tailwindcss';
`
['start-full', 'inset-s-full'],
['-start-full', '-inset-s-full'],
['start-auto', 'inset-s-auto'],
['start-px', 'inset-s-px'],
['-start-px', '-inset-s-px'],
['start-8', 'inset-s-8'], // Within default spacing scale
['-start-8', '-inset-s-8'], // Within default spacing scale
['start-123', 'inset-s-123'], // Outside of default spacing scale
['-start-123', '-inset-s-123'], // Outside of default spacing scale
await expectCanonicalization(input, candidate, expected)
})
['end-full', 'inset-e-full'],
['-end-full', '-inset-e-full'],
['end-auto', 'inset-e-auto'],
['end-px', 'inset-e-px'],
['-end-px', '-inset-e-px'],
['end-8', 'inset-e-8'], // Within default spacing scale
['-end-8', '-inset-e-8'], // Within default spacing scale
['end-123', 'inset-e-123'], // Outside of default spacing scale
['-end-123', '-inset-e-123'], // Outside of default spacing scale
]
test('`order-none` → `order-none` with custom implementation', { timeout }, async () => {
let candidate = 'order-none'
let expected = 'order-none'
// Creating a shared CSS file such that we can re-use the same design system
// for all of these.
let customImplementations = deprecated
.map(
([candidate]) => css`
@utility ${candidate} {
--custom-${randomUUID()}: implementation;
}
`,
)
.join('\n')
let input = css`
@import 'tailwindcss';
for (let [candidate, expected] of deprecated) {
test(`\`${candidate}\` → \`${expected}\` (%#)`, { timeout }, async () => {
let input = css`
@import 'tailwindcss';
`
@utility order-none {
order: none; /* imagine this exists */
}
`
await expectCanonicalization(input, candidate, expected)
})
await expectCanonicalization(input, candidate, expected)
})
test(
`\`${candidate}\` → \`${candidate}\` because of custom implementation (%#)`,
{ timeout },
async () => {
let input = css`
@import 'tailwindcss';
test('`break-words` → `wrap-break-word`', { timeout }, async () => {
let candidate = 'break-words'
let expected = 'wrap-break-word'
${customImplementations}
`
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
test('`[overflow-wrap:break-word]` → `wrap-break-word`', { timeout }, async () => {
let candidate = '[overflow-wrap:break-word]'
let expected = 'wrap-break-word'
let input = css`
@import 'tailwindcss';
`
await expectCanonicalization(input, candidate, expected)
})
test('`break-words` → `break-words` with custom implementation', { timeout }, async () => {
let candidate = 'break-words'
let expected = 'break-words'
let input = css`
@import 'tailwindcss';
@utility break-words {
break: words; /* imagine this exists */
}
`
await expectCanonicalization(input, candidate, expected)
})
await expectCanonicalization(input, candidate, candidate)
},
)
}
})
describe('arbitrary variants', () => {

View file

@ -198,7 +198,6 @@ export function canonicalizeCandidates(
}
function collapseCandidates(options: InternalCanonicalizeOptions, candidates: string[]): string[] {
if (candidates.length <= 1) return candidates
let designSystem = options.designSystem
// To keep things simple, we group candidates such that we only collapse
@ -1428,8 +1427,28 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO
const DEPRECATION_MAP = new Map([
['order-none', 'order-0'],
['break-words', 'wrap-break-word'],
['overflow-ellipsis', 'text-ellipsis'],
])
const DEPRECATION_TRANSFORMATION_MAP = new Map([
[/^(-)?start-(.*?)$/, '$1inset-s-$2'],
[/^(-)?end-(.*?)$/, '$1inset-e-$2'],
])
function* tryDeprecatedUtilities(candidate: string) {
// Try static replacements
let replacement = DEPRECATION_MAP.get(candidate)
if (replacement) yield replacement
// Try dynamic replacements
for (let [searchValue, replaceValue] of DEPRECATION_TRANSFORMATION_MAP) {
let replacement = candidate.replace(searchValue, replaceValue)
if (replacement === candidate) continue
yield replacement
}
}
function deprecatedUtilities(
candidate: Candidate,
options: InternalCanonicalizeOptions,
@ -1439,20 +1458,21 @@ function deprecatedUtilities(
let targetCandidateString = printUnprefixedCandidate(designSystem, candidate)
let replacementString = DEPRECATION_MAP.get(targetCandidateString) ?? null
if (replacementString === null) return candidate
let legacySignature = signatures.get(targetCandidateString)
if (typeof legacySignature !== 'string') return candidate
let replacementSignature = signatures.get(replacementString)
if (typeof replacementSignature !== 'string') return candidate
for (let replacementString of tryDeprecatedUtilities(targetCandidateString)) {
let replacementSignature = signatures.get(replacementString)
if (typeof replacementSignature !== 'string') continue
// Not the same signature, not safe to migrate
if (legacySignature !== replacementSignature) return candidate
// Not the same signature, not safe to migrate
if (legacySignature !== replacementSignature) continue
let [replacement] = parseCandidate(designSystem, replacementString)
return replacement
let [replacement] = parseCandidate(designSystem, replacementString)
return replacement
}
return candidate
}
// ----