Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- _Upgrade (experimental)_: Migrate `theme(…)` calls in classes to `var(…)` or to the modern `theme(…)` syntax ([#14664](https://github.com/tailwindlabs/tailwindcss/pull/14664))

### Fixed

- Ensure `theme` values defined outside of `extend` in JS configuration files overwrite all existing values for that namespace ([#14672](https://github.com/tailwindlabs/tailwindcss/pull/14672))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import { expect, test } from 'vitest'
import { themeToVar } from './theme-to-var'

test.each([
// Keep candidates that don't contain `theme(…)` or `theme(…, …)`
['[color:red]', '[color:red]'],

// Convert to `var(…)` if we can resolve the path
['[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-[var(--color-red-500)]'], // Arbitrary value
['bg-[size:theme(spacing.4)]', 'bg-[size:var(--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-[var(--color-red-500,_red)]'],

// Keep `theme(…)` if we can't resolve the path
['bg-[theme(colors.foo.1000)]', 'bg-[theme(colors.foo.1000)]'],

// Keep `theme(…)` if we can't resolve the path, but still try to convert the
// fallback value.
[
'bg-[theme(colors.foo.1000,theme(colors.red.500))]',
'bg-[theme(colors.foo.1000,var(--color-red-500))]',
],

// Use `theme(…)` (deeply nested) inside of a `calc(…)` function
['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--font-size-xs)_*_2)]'],

// Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)`
// - Can't convert to `var(…)` because that would lose the modifier.
// - Can't convert to a candidate modifier because there are multiple
// `theme(…)` calls.
//
// If we really want to, we can make a fancy migration that tries to move it
// 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(colors.red.500/50,theme(colors.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%`),
// to a candidate modifier.
// Arbitrary property, with simple percentage modifier
['[color:theme(colors.red.500/75%)]', '[color:var(--color-red-500)]/75'],

// Arbitrary property, with numbers (0-1) without a unit
['[color:theme(colors.red.500/.12)]', '[color:var(--color-red-500)]/12'],
['[color:theme(colors.red.500/0.12)]', '[color:var(--color-red-500)]/12'],

// Arbitrary property, with more complex modifier (we only allow whole numbers
// as bare modifiers). Convert the complex numbers to arbitrary values instead.
['[color:theme(colors.red.500/12.34%)]', '[color:var(--color-red-500)]/[12.34%]'],
['[color:theme(colors.red.500/var(--opacity))]', '[color:var(--color-red-500)]/[var(--opacity)]'],
['[color:theme(colors.red.500/.12345)]', '[color:var(--color-red-500)]/[12.345]'],
['[color:theme(colors.red.500/50.25%)]', '[color:var(--color-red-500)]/[50.25%]'],

// Arbitrary value
['bg-[theme(colors.red.500/75%)]', 'bg-[var(--color-red-500)]/75'],
['bg-[theme(colors.red.500/12.34%)]', 'bg-[var(--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'],

// 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'],

// Variants, we can't use `var(…)` especially inside of `@media(…)`. We can
// still upgrade the `theme(…)` to the modern syntax.
['max-[theme(spacing.4)]:flex', 'max-[theme(--spacing-4)]:flex'],

// This test in itself doesn't make much sense. But we need to make sure
// that this doesn't end up as the modifier in the candidate itself.
['max-[theme(spacing.4/50)]:flex', 'max-[theme(--spacing-4/50)]:flex'],

// `theme(…)` calls valid in v3, but not in v4 should still be converted.
['[--foo:theme(fontWeight.semibold)]', '[--foo:theme(fontWeight.semibold)]'],

// Invalid cases
['[--foo:theme(colors.red.500/50/50)]', '[--foo:theme(colors.red.500/50/50)]'],
['[--foo:theme(colors.red.500/50/50)]/50', '[--foo:theme(colors.red.500/50/50)]/50'],

// Partially invalid cases
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]',
],
[
'[--foo:theme(colors.red.500/50/50)_theme(colors.blue.200)]/50',
'[--foo:theme(colors.red.500/50/50)_var(--color-blue-200)]/50',
],
])('%s => %s', async (candidate, result) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So there's a class of theme() keypaths that are valid in v3 (and are valid in our interop layer) but don't have a keypath equivalent in v4. Things like: theme(fontWeight.semibold) or theme(cursor.progress). Let's add some tests here to see they are still migrated.

Maybe what we can do is to use the dict in default-theme.ts and resolve these to the actual values?

Copy link
Member Author

@RobinMalfait RobinMalfait Oct 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While on call, one thing we noticed is that theme(fontWeight.semibold) is hardcoded in the font-weight utility definition to be 600 and doesn't have a corresponding CSS variable. This means that we can't migrate theme(fontWeight.semibold) to a var(…) or even a theme(--font-weight-semibold) because there is no CSS variable. We could inline it to 600 directly since it's hardcoded.

However, there is a catch... if you define --font-weight-semibold: 100 as a CSS variable inside your @theme { … }, then the value is not hardcoded anymore, but the CSS variable takes precedence.

This means that we will migrate theme(fontWeight.semibold) to theme(--font-weight-semibold) or even var(--font-weight-semibold) but only if you have this CSS variable configured.

Long story short:

  1. If you migrate to var(…) or theme(--…), and remove the CSS variable from your @theme { … } later, then the default fontWeight.semibold won't apply.
  2. If you don't migrate, but inline the value theme(fontWeight.semibold) becomes 600. Then adding the CSS variable to your @theme { … } later, then the variable won't take effect because we already hardcoded the value to 600.
  3. Keep it as-is which is the more correct thing to do I think, but then we have some theme(…) calls with dot notation, some with --… notation and some var(…) notation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the safer thing to do here is to:

  1. Migrate if we a variable was configured (that's what will happen by default)
  2. Keep the old syntax if we can't instead of inlining the value.

let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})

expect(themeToVar(designSystem, {}, candidate)).toEqual(result)
})
Loading