Migrate theme(…) to --theme(…), migrate calc(var(--spacing)*x) to --spacing(x) (#15579)

This PR improves the codemod tool to simplify 2 things:

1. Whenever you have a `theme(…)` call, we try to change it to a
`var(…)`, but if that doesn't work for some reason, we will make sure to
at least convert it to the more modern `--theme(…)`.
2. When converting `theme(spacing.2)`, we used to convert it to
`calc(var(--spacing)*2)`, but now we will convert it to `--spacing(2)`
instead.
This commit is contained in:
Robin Malfait 2025-01-09 17:17:07 +01:00 committed by GitHub
parent c766d7e274
commit 82589eb2c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 35 additions and 31 deletions

View File

@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allow negative utility names in `@utilty` ([#15573](https://github.com/tailwindlabs/tailwindcss/pull/15573))
- Remove all `@keyframes` contributed by JavaScript plugins when using `@reference` imports ([#15581](https://github.com/tailwindlabs/tailwindcss/pull/15581))
- _Upgrade (experimental)_: Do not extract class names from functions (e.g. `shadow` in `filter: 'drop-shadow(…)'`) ([#15566](https://github.com/tailwindlabs/tailwindcss/pull/15566))
- _Upgrade (experimental)_: Migrate `theme(spacing.2)` to `--spacing(2)` ([#15579](https://github.com/tailwindlabs/tailwindcss/pull/15579))
- _Upgrade (experimental)_: Migrate `theme(…)` to `--theme(…)` ([#15579](https://github.com/tailwindlabs/tailwindcss/pull/15579))
### Changed

View File

@ -1296,10 +1296,10 @@ describe('border compatibility', () => {
"
--- src/index.html ---
<div
class="[width:calc(var(--spacing)*2)]
[width:calc(var(--spacing)*4.5)]
class="[width:--spacing(2)]
[width:--spacing(4.5)]
[width:var(--spacing-5_5)]
[width:calc(var(--spacing)*13)]
[width:--spacing(13)]
[width:var(--spacing-100)]
[width:var(--spacing-miami)]"
></div>
@ -1332,10 +1332,10 @@ describe('border compatibility', () => {
}
.container {
width: calc(var(--spacing) * 2);
width: calc(var(--spacing) * 4.5);
width: --spacing(2);
width: --spacing(4.5);
width: var(--spacing-5_5);
width: calc(var(--spacing) * 13);
width: --spacing(13);
width: var(--spacing-100);
width: var(--spacing-miami);
}
@ -1515,7 +1515,7 @@ describe('border compatibility', () => {
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= theme(--breakpoint-sm)) {
@media (width >= --theme(--breakpoint-sm)) {
max-width: none;
}
@media (width >= 48rem) {

View File

@ -35,11 +35,11 @@ it('should migrate `theme(…)` to `var(…)`', async () => {
}
`),
).toMatchInlineSnapshot(`
"@media theme(--breakpoint-sm) {
"@media --theme(--breakpoint-sm) {
.foo {
background-color: var(--color-red-900);
color: theme(--color-red-900 / 75%);
border-color: theme(--color-red-200 / 75%);
color: --theme(--color-red-900 / 75%);
border-color: --theme(--color-red-200 / 75%);
}
}"
`)

View File

@ -36,6 +36,10 @@ test.each([
['supports-[--test]:flex', 'supports-(--test):flex'],
['supports-[_--test]:flex', 'supports-[--test]:flex'],
// Custom CSS functions that look like variables should not be converted
['w-[--spacing(5)]', 'w-[--spacing(5)]'],
['bg-[--theme(--color-red-500)]', 'bg-[--theme(--color-red-500)]'],
// Some properties never had var() injection in v3.
['[scroll-timeline-name:--myTimeline]', '[scroll-timeline-name:--myTimeline]'],
['[timeline-scope:--myScope]', '[timeline-scope:--myScope]'],

View File

@ -74,13 +74,14 @@ export function automaticVarInjection(
function injectVar(value: string): { value: string; didChange: boolean } {
let didChange = false
if (value.startsWith('--')) {
if (value.startsWith('--') && !value.includes('(')) {
value = `var(${value})`
didChange = true
} else if (value.startsWith(' --')) {
value = value.slice(1)
didChange = true
}
return { value, didChange }
}

View File

@ -9,9 +9,9 @@ test.each([
['[color:red]', '[color:red]'],
// Handle special cases around `.1` in the `theme(…)`
['[--value:theme(spacing.1)]', '[--value:calc(var(--spacing)*1)]'],
['[--value:theme(spacing.1)]', '[--value:--spacing(1)]'],
['[--value:theme(fontSize.xs.1.lineHeight)]', '[--value:var(--text-xs--line-height)]'],
['[--value:theme(spacing[1.25])]', '[--value:calc(var(--spacing)*1.25)]'],
['[--value:theme(spacing[1.25])]', '[--value:--spacing(1.25)]'],
// Should not convert invalid spacing values to calc
['[--value:theme(spacing[1.1])]', '[--value:theme(spacing[1.1])]'],
@ -20,7 +20,7 @@ test.each([
['[color:theme(colors.red.500)]', '[color:var(--color-red-500)]'], // Arbitrary property
['[color:theme(colors.red.500)]/50', '[color:var(--color-red-500)]/50'], // Arbitrary property + modifier
['bg-[theme(colors.red.500)]', 'bg-(--color-red-500)'], // Arbitrary value
['bg-[size:theme(spacing.4)]', 'bg-[size:calc(var(--spacing)*4)]'], // Arbitrary value + data type hint
['bg-[size:theme(spacing.4)]', 'bg-[size:--spacing(4)]'], // Arbitrary value + data type hint
// Convert to `var(…)` if we can resolve the path, but keep fallback values
['bg-[theme(colors.red.500,red)]', 'bg-(--color-red-500,red)'],
@ -47,11 +47,11 @@ test.each([
// to a candidate modifier _if_ all `theme(…)` calls use the same modifier.
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]',
'[color:theme(--color-red-500/50,theme(--color-blue-500/50))]',
'[color:--theme(--color-red-500/50,--theme(--color-blue-500/50))]',
],
[
'[color:theme(colors.red.500/50,theme(colors.blue.500/50))]/50',
'[color:theme(--color-red-500/50,theme(--color-blue-500/50))]/50',
'[color:--theme(--color-red-500/50,--theme(--color-blue-500/50))]/50',
],
// Convert the `theme(…)`, but try to move the inline modifier (e.g. `50%`),
@ -75,22 +75,22 @@ test.each([
['bg-[theme(colors.red.500/12.34%)]', 'bg-(--color-red-500)/[12.34%]'],
// Arbitrary property that already contains a modifier
['[color:theme(colors.red.500/50%)]/50', '[color:theme(--color-red-500/50%)]/50'],
['[color:theme(colors.red.500/50%)]/50', '[color:--theme(--color-red-500/50%)]/50'],
// Values that don't contain only `theme(…)` calls should not be converted to
// use a modifier since the color is not the whole value.
[
'shadow-[shadow:inset_0px_1px_theme(colors.white/15%)]',
'shadow-[shadow:inset_0px_1px_theme(--color-white/15%)]',
'shadow-[shadow:inset_0px_1px_--theme(--color-white/15%)]',
],
// Arbitrary value, where the candidate already contains a modifier
// This should still migrate the `theme(…)` syntax to the modern syntax.
['bg-[theme(colors.red.500/50%)]/50', 'bg-[theme(--color-red-500/50%)]/50'],
['bg-[theme(colors.red.500/50%)]/50', 'bg-[--theme(--color-red-500/50%)]/50'],
// Variants, we can't use `var(…)` especially inside of `@media(…)`. We can
// still upgrade the `theme(…)` to the modern syntax.
['max-[theme(screens.lg)]:flex', 'max-[theme(--breakpoint-lg)]:flex'],
['max-[theme(screens.lg)]:flex', 'max-[--theme(--breakpoint-lg)]:flex'],
// There are no variables for `--spacing` multiples, so we can't convert this
['max-[theme(spacing.4)]:flex', 'max-[theme(spacing.4)]:flex'],
@ -100,10 +100,7 @@ test.each([
// `theme(…)` calls in another CSS function is replaced correctly.
// Additionally we remove unnecessary whitespace.
[
'grid-cols-[min(50%_,_theme(spacing.80))_auto]',
'grid-cols-[min(50%,calc(var(--spacing)*80))_auto]',
],
['grid-cols-[min(50%_,_theme(spacing.80))_auto]', 'grid-cols-[min(50%,--spacing(80))_auto]'],
// `theme(…)` calls valid in v3, but not in v4 should still be converted.
['[--foo:theme(transitionDuration.500)]', '[--foo:theme(transitionDuration.500)]'],
@ -152,7 +149,7 @@ test('extended space scale converts to var or calc', async () => {
},
)
expect(themeToVar(designSystem, {}, '[--value:theme(spacing.1)]')).toEqual(
'[--value:calc(var(--spacing)*1)]',
'[--value:--spacing(1)]',
)
expect(themeToVar(designSystem, {}, '[--value:theme(spacing.2)]')).toEqual(
'[--value:var(--spacing-2)]',

View File

@ -148,9 +148,7 @@ export function createConverter(designSystem: DesignSystem, { prettyPrint = fals
let parts = segment(path, '/').map((part) => part.trim())
// Multiple `/` separators, which makes this an invalid path
if (parts.length > 2) {
return null
}
if (parts.length > 2) return null
// The path contains a `/`, which means that there is a modifier such as
// `theme(colors.red.500/50%)`.
@ -212,7 +210,7 @@ export function createConverter(designSystem: DesignSystem, { prettyPrint = fals
let multiplier = keyPath[1]
if (!isValidSpacingMultiplier(multiplier)) return null
return 'calc(var(--spacing) * ' + multiplier + ')'
return `--spacing(${multiplier})`
}
return null
@ -227,7 +225,9 @@ export function createConverter(designSystem: DesignSystem, { prettyPrint = fals
let modifier =
parts.length > 0 ? (prettyPrint ? ` / ${parts.join(' / ')}` : `/${parts.join('/')}`) : ''
return fallback ? `theme(${variable}${modifier}, ${fallback})` : `theme(${variable}${modifier})`
return fallback
? `--theme(${variable}${modifier}, ${fallback})`
: `--theme(${variable}${modifier})`
}
return convert

View File

@ -60,7 +60,7 @@ export function buildCustomContainerUtilityRules(
let [key] = breakpoints[0]
// Unset all default breakpoints
rules.push(
atRule('@media', `(width >= theme(--breakpoint-${key}))`, [decl('max-width', 'none')]),
atRule('@media', `(width >= --theme(--breakpoint-${key}))`, [decl('max-width', 'none')]),
)
}