Stop suggesting legacy utilities (#19169)

It was already supposed to work this way — and it appeared via the tests
that it did — but the tests weren't loading the design system normally
so when we split the legacy utilities into a separate file it stopped
testing this properly.

The setup here is a bit iffy but the gist is:
- We allow static utilities to be suggested by default
- A static utility can *opt out* of suggestions by explicitly
registering themselves to return none.
This commit is contained in:
Jordan Pittman 2025-10-21 06:54:10 -04:00 • committed by GitHub
parent 7537e34fd1
commit 3a4ab8201b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 74 additions and 36 deletions

View file

@ -7576,7 +7576,6 @@ exports[`getClassList 1`] = `
"max-w-min",
"max-w-none",
"max-w-px",
"max-w-screen",
"max-w-svh",
"max-w-svw",
"mb-0",

View file

@ -581,6 +581,17 @@ describe.each([['default'], ['with-variant'], ['important'], ['prefix']])('%s',
await expectCanonicalization(input, candidate, expected)
})
test('`[overflow-wrap:break-word]` → `wrap-break-word`', 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', async () => {
let candidate = 'break-words'
let expected = 'break-words'

View file

@ -13,6 +13,7 @@ export function registerLegacyUtilities(designSystem: DesignSystem) {
['l', 'left'],
['tl', 'top left'],
]) {
designSystem.utilities.suggest(`bg-gradient-to-${value}`, () => [])
designSystem.utilities.static(`bg-gradient-to-${value}`, () => [
decl('--tw-gradient-position', `to ${direction} in oklab`),
decl('background-image', `linear-gradient(var(--tw-gradient-stops))`),
@ -20,25 +21,34 @@ export function registerLegacyUtilities(designSystem: DesignSystem) {
}
// Legacy `background-position` utilities for compatibility with v4.0 and earlier
designSystem.utilities.suggest('bg-left-top', () => [])
designSystem.utilities.static('bg-left-top', () => [decl('background-position', 'left top')])
designSystem.utilities.suggest('bg-right-top', () => [])
designSystem.utilities.static('bg-right-top', () => [decl('background-position', 'right top')])
designSystem.utilities.suggest('bg-left-bottom', () => [])
designSystem.utilities.static('bg-left-bottom', () => [
decl('background-position', 'left bottom'),
])
designSystem.utilities.suggest('bg-right-bottom', () => [])
designSystem.utilities.static('bg-right-bottom', () => [
decl('background-position', 'right bottom'),
])
// Legacy `object-position` utilities for compatibility with v4.0 and earlier
designSystem.utilities.suggest('object-left-top', () => [])
designSystem.utilities.static('object-left-top', () => [decl('object-position', 'left top')])
designSystem.utilities.suggest('object-right-top', () => [])
designSystem.utilities.static('object-right-top', () => [decl('object-position', 'right top')])
designSystem.utilities.suggest('object-left-bottom', () => [])
designSystem.utilities.static('object-left-bottom', () => [
decl('object-position', 'left bottom'),
])
designSystem.utilities.suggest('object-right-bottom', () => [])
designSystem.utilities.static('object-right-bottom', () => [
decl('object-position', 'right bottom'),
])
designSystem.utilities.suggest('max-w-screen', () => [])
designSystem.utilities.functional('max-w-screen', (candidate) => {
if (!candidate.value) return
if (candidate.value.kind === 'arbitrary') return
@ -47,18 +57,22 @@ export function registerLegacyUtilities(designSystem: DesignSystem) {
return [decl('max-width', value)]
})
designSystem.utilities.suggest('overflow-ellipsis', () => [])
designSystem.utilities.static(`overflow-ellipsis`, () => [decl('text-overflow', `ellipsis`)])
designSystem.utilities.suggest('decoration-slice', () => [])
designSystem.utilities.static(`decoration-slice`, () => [
decl('-webkit-box-decoration-break', `slice`),
decl('box-decoration-break', `slice`),
])
designSystem.utilities.suggest('decoration-clone', () => [])
designSystem.utilities.static(`decoration-clone`, () => [
decl('-webkit-box-decoration-break', `clone`),
decl('box-decoration-break', `clone`),
])
designSystem.utilities.suggest('flex-shrink', () => [])
designSystem.utilities.functional('flex-shrink', (candidate) => {
if (candidate.modifier) return
@ -75,6 +89,7 @@ export function registerLegacyUtilities(designSystem: DesignSystem) {
}
})
designSystem.utilities.suggest('flex-grow', () => [])
designSystem.utilities.functional('flex-grow', (candidate) => {
if (candidate.modifier) return
@ -91,6 +106,9 @@ export function registerLegacyUtilities(designSystem: DesignSystem) {
}
})
designSystem.utilities.suggest('order-none', () => [])
designSystem.utilities.static('order-none', () => [decl('order', '0')])
designSystem.utilities.suggest('break-words', () => [])
designSystem.utilities.static('break-words', () => [decl('overflow-wrap', 'break-word')])
}

View file

@ -1,35 +1,36 @@
import { expect, test } from 'vitest'
import { __unstable__loadDesignSystem } from '.'
import { buildDesignSystem } from './design-system'
import plugin from './plugin'
import { Theme, ThemeOptions } from './theme'
import { ThemeOptions } from './theme'
const css = String.raw
function loadDesignSystem() {
let theme = new Theme()
theme.add('--spacing', '0.25rem')
theme.add('--colors-red-500', 'red')
theme.add('--colors-blue-500', 'blue')
theme.add('--breakpoint-sm', '640px')
theme.add('--aspect-video', '16 / 9')
theme.add('--font-sans', 'sans-serif')
theme.add('--font-weight-superbold', '900')
theme.add('--text-xs', '0.75rem')
theme.add('--text-xs--line-height', '1rem')
theme.add('--perspective-dramatic', '100px')
theme.add('--perspective-normal', '500px')
theme.add('--opacity-background', '0.3')
theme.add('--drop-shadow-sm', '0 1px 1px rgb(0 0 0 / 0.05)')
theme.add('--inset-shadow-sm', 'inset 0 1px 1px rgb(0 0 0 / 0.05)')
theme.add('--font-weight-bold', '700')
theme.add('--container-md', '768px')
theme.add('--container-lg', '1024px')
return buildDesignSystem(theme)
return __unstable__loadDesignSystem(`
@theme {
--spacing: 0.25rem;
--colors-red-500: red;
--colors-blue-500: blue;
--breakpoint-sm: 640px;
--aspect-video: 16 / 9;
--font-sans: sans-serif;
--font-weight-superbold: 900;
--text-xs: 0.75rem;
--text-xs--line-height: 1rem;
--perspective-dramatic: 100px;
--perspective-normal: 500px;
--opacity-background: 0.3;
--drop-shadow-sm: 0 1px 1px rgb(0 0 0 / 0.05);
--inset-shadow-sm: inset 0 1px 1px rgb(0 0 0 / 0.05);
--font-weight-bold: 700;
--container-md: 768px;
--container-lg: 1024px;
}
`)
}
test('getClassList', () => {
let design = loadDesignSystem()
test('getClassList', async () => {
let design = await loadDesignSystem()
let classList = design.getClassList()
let classNames = classList.flatMap(([name, meta]) => [
name,
@ -39,8 +40,8 @@ test('getClassList', () => {
expect(classNames).toMatchSnapshot()
})
test('Spacing utilities do not suggest bare values when not using the multiplier-based spacing scale', () => {
let design = loadDesignSystem()
test('Spacing utilities do not suggest bare values when not using the multiplier-based spacing scale', async () => {
let design = await loadDesignSystem()
// Remove spacing scale
design.theme.clearNamespace('--spacing', ThemeOptions.NONE)
@ -58,22 +59,22 @@ test('Spacing utilities do not suggest bare values when not using the multiplier
expect(classNames).not.toContain('p-4')
})
test('Theme values with underscores are converted back to decimal points', () => {
let design = loadDesignSystem()
test('Theme values with underscores are converted back to decimal points', async () => {
let design = await loadDesignSystem()
let classes = design.getClassList()
expect(classes).toContainEqual(['inset-0.5', { modifiers: [] }])
})
test('getVariants', () => {
let design = loadDesignSystem()
test('getVariants', async () => {
let design = await loadDesignSystem()
let variants = design.getVariants()
expect(variants).toMatchSnapshot()
})
test('getVariants compound', () => {
let design = loadDesignSystem()
test('getVariants compound', async () => {
let design = await loadDesignSystem()
let variants = design.getVariants()
let group = variants.find((v) => v.name === 'group')!
@ -130,16 +131,16 @@ test('variant selectors are in the correct order', async () => {
`)
})
test('The variant `has-force` does not crash', () => {
let design = loadDesignSystem()
test('The variant `has-force` does not crash', async () => {
let design = await loadDesignSystem()
let variants = design.getVariants()
let has = variants.find((v) => v.name === 'has')!
expect(has.selectors({ value: 'force' })).toMatchInlineSnapshot(`[]`)
})
test('Can produce CSS per candidate using `candidatesToCss`', () => {
let design = loadDesignSystem()
test('Can produce CSS per candidate using `candidatesToCss`', async () => {
let design = await loadDesignSystem()
design.invalidCandidates = new Set(['bg-[#fff]'])
expect(design.candidatesToCss(['underline', 'i-dont-exist', 'bg-[#fff]', 'bg-[#000]', 'text-xs']))

View file

@ -31,6 +31,9 @@ export function getClassList(design: DesignSystem): ClassEntry[] {
// Static utilities only work as-is
for (let utility of design.utilities.keys('static')) {
let completions = design.utilities.getCompletions(utility)
if (completions.length === 0) continue
let item = items.get(utility)
item.fraction = false
item.modifiers = []

View file

@ -122,6 +122,12 @@ export class Utilities {
}
getCompletions(name: string): SuggestionGroup[] {
if (this.has(name, 'static')) {
return (
this.completions.get(name)?.() ?? [{ supportsNegative: false, values: [], modifiers: [] }]
)
}
return this.completions.get(name)?.() ?? []
}