Change casing of utilities with named values to kebab-case to match u… (#18017)

Fixes #16156

## Summary

This PR adds a new 3 -> 4 template migration that changes the casing of
in both utility values and modifier values from camelCase to kebab-case
to match the updated CSS variable names.

## Test plan

- Added integration test, see the diff in the PR.
This commit is contained in:
Philipp Spiess 2025-05-14 14:51:51 +02:00 • committed by GitHub
parent 4db711d1e4
commit e57a2f5a3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 93 additions and 3 deletions

View file

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `lightningcss` now statically links Visual Studio redistributables ([#17979](https://github.com/tailwindlabs/tailwindcss/pull/17979))
- Ensure that running the Standalone build does not leave temporary files behind ([#17981](https://github.com/tailwindlabs/tailwindcss/pull/17981))
- Fix `-rotate-*` utilities with arbitrary values ([#18014](https://github.com/tailwindlabs/tailwindcss/pull/18014))
- Upgrade: Change casing of utilities with named values to kebab-case to match updated theme variables ([#18017](https://github.com/tailwindlabs/tailwindcss/pull/18017))
### Added

View file

@ -30,9 +30,13 @@ test(
400: '#f87171',
500: 'red',
},
superRed: '#ff0000',
steel: 'rgb(70 130 180 / <alpha-value>)',
smoke: 'rgba(245, 245, 245, var(--smoke-alpha, <alpha-value>))',
},
opacity: {
superOpaque: '0.95',
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.5rem' }],
@ -144,9 +148,10 @@ test(
}
`,
'src/index.html': html`
<div
<div
class="[letter-spacing:theme(letterSpacing.superWide)] [line-height:theme(lineHeight.superLoose)]"
></div>
<div class="text-red-superRed/superOpaque leading-superLoose"></div>
`,
'node_modules/my-external-lib/src/template.html': html`
<div class="text-red-500">
@ -162,8 +167,9 @@ test(
"
--- src/index.html ---
<div
class="[letter-spacing:var(--tracking-super-wide)] [line-height:var(--leading-super-loose)]"
></div>
class="[letter-spacing:var(--tracking-super-wide)] [line-height:var(--leading-super-loose)]"
></div>
<div class="text-red-super-red/super-opaque leading-super-loose"></div>
--- src/input.css ---
@import 'tailwindcss';
@ -181,9 +187,13 @@ test(
--color-red-500: #ef4444;
--color-red-600: #dc2626;
--color-super-red: #ff0000;
--color-steel: rgb(70 130 180);
--color-smoke: rgba(245, 245, 245, var(--smoke-alpha, 1));
--opacity-*: initial;
--opacity-super-opaque: 95%;
--text-*: initial;
--text-xs: 0.75rem;
--text-xs--line-height: 1rem;

View file

@ -0,0 +1,24 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { expect, test, vi } from 'vitest'
import * as versions from '../../utils/version'
import { migrateCamelcaseInNamedValue } from './migrate-camelcase-in-named-value'
vi.spyOn(versions, 'isMajor').mockReturnValue(true)
test.each([
['text-superRed', 'text-super-red'],
['text-red/superOpaque', 'text-red/super-opaque'],
['text-superRed/superOpaque', 'text-super-red/super-opaque'],
// Should not migrate named values in modifiers
['group-hover/superGroup:underline', 'group-hover/superGroup:underline'],
['hover:text-superRed', 'hover:text-super-red'],
['hover:text-red/superOpaque', 'hover:text-red/super-opaque'],
['hover:text-superRed/superOpaque', 'hover:text-super-red/super-opaque'],
])('%s => %s', async (candidate, result) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})
expect(migrateCamelcaseInNamedValue(designSystem, {}, candidate)).toEqual(result)
})

View file

@ -0,0 +1,53 @@
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import * as version from '../../utils/version'
// Converts named values to use kebab-case. This is necessary because the
// upgrade tool also renames the theme values to kebab-case, so `text-superRed`
// will have its theme value renamed to `--color-super-red` and thus the utility
// will be renamed to `text-super-red`.
export function migrateCamelcaseInNamedValue(
designSystem: DesignSystem,
_userConfig: Config | null,
rawCandidate: string,
): string {
if (!version.isMajor(3)) return rawCandidate
for (let candidate of designSystem.parseCandidate(rawCandidate)) {
if (candidate.kind !== 'functional') continue
let clone = structuredClone(candidate)
let didChange = false
if (
candidate.value &&
clone.value &&
candidate.value.kind === 'named' &&
clone.value.kind === 'named' &&
candidate.value.value.match(/[A-Z]/)
) {
clone.value.value = camelToKebab(candidate.value.value)
didChange = true
}
if (
candidate.modifier &&
clone.modifier &&
candidate.modifier.kind === 'named' &&
clone.modifier.kind === 'named' &&
candidate.modifier.value.match(/[A-Z]/)
) {
clone.modifier.value = camelToKebab(candidate.modifier.value)
didChange = true
}
if (didChange) {
return designSystem.printCandidate(clone)
}
}
return rawCandidate
}
function camelToKebab(str: string): string {
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
}

View file

@ -11,6 +11,7 @@ import { migrateArbitraryVariants } from './migrate-arbitrary-variants'
import { migrateAutomaticVarInjection } from './migrate-automatic-var-injection'
import { migrateBareValueUtilities } from './migrate-bare-utilities'
import { migrateBgGradient } from './migrate-bg-gradient'
import { migrateCamelcaseInNamedValue } from './migrate-camelcase-in-named-value'
import { migrateDropUnnecessaryDataTypes } from './migrate-drop-unnecessary-data-types'
import { migrateEmptyArbitraryValues } from './migrate-handle-empty-arbitrary-values'
import { migrateImportant } from './migrate-important'
@ -41,6 +42,7 @@ export const DEFAULT_MIGRATIONS: Migration[] = [
migrateImportant,
migrateBgGradient,
migrateSimpleLegacyClasses,
migrateCamelcaseInNamedValue,
migrateLegacyClasses,
migrateMaxWidthScreen,
migrateThemeToVar,