Skip to content

Commit 88a2d22

Browse files
authored
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
1 parent e4856c9 commit 88a2d22

3 files changed

Lines changed: 90 additions & 67 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
- Upgrade: Use `config.content` when migrating from Tailwind CSS v3 to Tailwind CSS v4 ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
2828
- Upgrade: Never migrate files that are ignored by git ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
2929
- Add `.env` and `.env.*` to default ignored content files ([#19846](https://github.com/tailwindlabs/tailwindcss/pull/19846))
30+
- Canonicalization: migrate `overflow-ellipsis` into `text-ellipsis` ([#19849](https://github.com/tailwindlabs/tailwindcss/pull/19849))
31+
- 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))
32+
- 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))
3033

3134
## [4.2.2] - 2026-03-18
3235

packages/tailwindcss/src/canonicalize-candidates.test.ts

Lines changed: 57 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { randomUUID } from 'node:crypto'
12
import fs from 'node:fs'
23
import path from 'node:path'
34
import { describe, expect, test } from 'vitest'
@@ -611,68 +612,67 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
611612
})
612613

613614
describe('deprecated utilities', () => {
614-
test('`order-none` → `order-0`', { timeout }, async () => {
615-
let candidate = 'order-none'
616-
let expected = 'order-0'
617-
618-
let input = css`
619-
@import 'tailwindcss';
620-
`
621-
622-
await expectCanonicalization(input, candidate, expected)
623-
})
624-
625-
test('`order-none` → `order-none` with custom implementation', { timeout }, async () => {
626-
let candidate = 'order-none'
627-
let expected = 'order-none'
628-
629-
let input = css`
630-
@import 'tailwindcss';
631-
632-
@utility order-none {
633-
order: none; /* imagine this exists */
634-
}
635-
`
636-
637-
await expectCanonicalization(input, candidate, expected)
638-
})
639-
640-
test('`break-words` → `wrap-break-word`', { timeout }, async () => {
641-
let candidate = 'break-words'
642-
let expected = 'wrap-break-word'
643-
644-
let input = css`
645-
@import 'tailwindcss';
646-
`
647-
648-
await expectCanonicalization(input, candidate, expected)
649-
})
650-
651-
test('`[overflow-wrap:break-word]` → `wrap-break-word`', { timeout }, async () => {
652-
let candidate = '[overflow-wrap:break-word]'
653-
let expected = 'wrap-break-word'
654-
655-
let input = css`
656-
@import 'tailwindcss';
657-
`
615+
let deprecated = [
616+
['order-none', 'order-0'],
617+
['break-words', 'wrap-break-word'],
618+
['overflow-ellipsis', 'text-ellipsis'],
619+
620+
['start-full', 'inset-s-full'],
621+
['-start-full', '-inset-s-full'],
622+
['start-auto', 'inset-s-auto'],
623+
['start-px', 'inset-s-px'],
624+
['-start-px', '-inset-s-px'],
625+
['start-8', 'inset-s-8'], // Within default spacing scale
626+
['-start-8', '-inset-s-8'], // Within default spacing scale
627+
['start-123', 'inset-s-123'], // Outside of default spacing scale
628+
['-start-123', '-inset-s-123'], // Outside of default spacing scale
629+
630+
['end-full', 'inset-e-full'],
631+
['-end-full', '-inset-e-full'],
632+
['end-auto', 'inset-e-auto'],
633+
['end-px', 'inset-e-px'],
634+
['-end-px', '-inset-e-px'],
635+
['end-8', 'inset-e-8'], // Within default spacing scale
636+
['-end-8', '-inset-e-8'], // Within default spacing scale
637+
['end-123', 'inset-e-123'], // Outside of default spacing scale
638+
['-end-123', '-inset-e-123'], // Outside of default spacing scale
639+
]
640+
641+
// Creating a shared CSS file such that we can re-use the same design system
642+
// for all of these.
643+
let customImplementations = deprecated
644+
.map(
645+
([candidate]) => css`
646+
@utility ${candidate} {
647+
--custom-${randomUUID()}: implementation;
648+
}
649+
`,
650+
)
651+
.join('\n')
658652

659-
await expectCanonicalization(input, candidate, expected)
660-
})
653+
for (let [candidate, expected] of deprecated) {
654+
test(`\`${candidate}\` → \`${expected}\` (%#)`, { timeout }, async () => {
655+
let input = css`
656+
@import 'tailwindcss';
657+
`
661658

662-
test('`break-words` → `break-words` with custom implementation', { timeout }, async () => {
663-
let candidate = 'break-words'
664-
let expected = 'break-words'
659+
await expectCanonicalization(input, candidate, expected)
660+
})
665661

666-
let input = css`
667-
@import 'tailwindcss';
662+
test(
663+
`\`${candidate}\` → \`${candidate}\` because of custom implementation (%#)`,
664+
{ timeout },
665+
async () => {
666+
let input = css`
667+
@import 'tailwindcss';
668668
669-
@utility break-words {
670-
break: words; /* imagine this exists */
671-
}
672-
`
669+
${customImplementations}
670+
`
673671

674-
await expectCanonicalization(input, candidate, expected)
675-
})
672+
await expectCanonicalization(input, candidate, candidate)
673+
},
674+
)
675+
}
676676
})
677677

678678
describe('arbitrary variants', () => {

packages/tailwindcss/src/canonicalize-candidates.ts

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@ export function canonicalizeCandidates(
198198
}
199199

200200
function collapseCandidates(options: InternalCanonicalizeOptions, candidates: string[]): string[] {
201-
if (candidates.length <= 1) return candidates
202201
let designSystem = options.designSystem
203202

204203
// To keep things simple, we group candidates such that we only collapse
@@ -1428,8 +1427,28 @@ function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeO
14281427
const DEPRECATION_MAP = new Map([
14291428
['order-none', 'order-0'],
14301429
['break-words', 'wrap-break-word'],
1430+
['overflow-ellipsis', 'text-ellipsis'],
14311431
])
14321432

1433+
const DEPRECATION_TRANSFORMATION_MAP = new Map([
1434+
[/^(-)?start-(.*?)$/, '$1inset-s-$2'],
1435+
[/^(-)?end-(.*?)$/, '$1inset-e-$2'],
1436+
])
1437+
1438+
function* tryDeprecatedUtilities(candidate: string) {
1439+
// Try static replacements
1440+
let replacement = DEPRECATION_MAP.get(candidate)
1441+
if (replacement) yield replacement
1442+
1443+
// Try dynamic replacements
1444+
for (let [searchValue, replaceValue] of DEPRECATION_TRANSFORMATION_MAP) {
1445+
let replacement = candidate.replace(searchValue, replaceValue)
1446+
if (replacement === candidate) continue
1447+
1448+
yield replacement
1449+
}
1450+
}
1451+
14331452
function deprecatedUtilities(
14341453
candidate: Candidate,
14351454
options: InternalCanonicalizeOptions,
@@ -1439,20 +1458,21 @@ function deprecatedUtilities(
14391458

14401459
let targetCandidateString = printUnprefixedCandidate(designSystem, candidate)
14411460

1442-
let replacementString = DEPRECATION_MAP.get(targetCandidateString) ?? null
1443-
if (replacementString === null) return candidate
1444-
14451461
let legacySignature = signatures.get(targetCandidateString)
14461462
if (typeof legacySignature !== 'string') return candidate
14471463

1448-
let replacementSignature = signatures.get(replacementString)
1449-
if (typeof replacementSignature !== 'string') return candidate
1464+
for (let replacementString of tryDeprecatedUtilities(targetCandidateString)) {
1465+
let replacementSignature = signatures.get(replacementString)
1466+
if (typeof replacementSignature !== 'string') continue
1467+
1468+
// Not the same signature, not safe to migrate
1469+
if (legacySignature !== replacementSignature) continue
14501470

1451-
// Not the same signature, not safe to migrate
1452-
if (legacySignature !== replacementSignature) return candidate
1471+
let [replacement] = parseCandidate(designSystem, replacementString)
1472+
return replacement
1473+
}
14531474

1454-
let [replacement] = parseCandidate(designSystem, replacementString)
1455-
return replacement
1475+
return candidate
14561476
}
14571477

14581478
// ----

0 commit comments

Comments
 (0)