Skip to content

Commit

Permalink
Migrate theme(…) calls to var(…) or the modern theme(…) syntax (#…
Browse files Browse the repository at this point in the history
…14664)

This PR adds a codemod to convert `theme(…)` calls to `var(…)` calls. If
we can't safely do this, then we try to convert the `theme(…)` syntax
(dot notation) to the modern `theme(…)` syntax (with CSS variable-like
syntax).

### Let's look at some examples:

**Simple example:**

Input:
```html
<div class="bg-[theme(colors.red.500)]"></div>
```

Output:
```html
<div class="bg-[var(--color-red-500)]"></div>
```

---

**With fallback:**

Input:
```html
<div class="bg-[theme(colors.red.500,theme(colors.blue.500))]"></div>
```

Output:
```html
<div class="bg-[var(--color-red-500,var(--color-blue-500))]"></div>
```

---

**With modifiers:**

Input:
```html
<div class="bg-[theme(colors.red.500/75%)]"></div>
```

Output:
```html
<div class="bg-[var(--color-red-500)]/75"></div>
```

We can special case this, because if you are using that modifier syntax
we _assume_ it's being used in a `theme(…)` call referencing a color.
This means that we can also convert it to a modifier on the actual
candidate.

---

**With modifier, if a modifier is already present:**

Input:
```html
<div class="bg-[theme(colors.red.500/75%)]/50"></div>
```

Output:
```html
<div class="bg-[theme(--color-red-500/75%)]/50"></div>
```

In this case we can't use the `var(…)` syntax because that requires us
to move the opacity modifier to the candidate itself. In this case we
could use math to figure out the expected modifier, but that might be
too confusing. Instead, we convert to the modern `theme(…)` syntax.

---

**Multiple `theme(…)` calls with modifiers:**

Input:
```html
<div class="bg-[theme(colors.red.500/75%,theme(colors.blue.500/50%))]"></div>
```

Output:
```html
<div class="bg-[theme(--color-red-500/75%,theme(--color-blue-500/50%))]"></div>
```

In this case we can't convert to `var(…)` syntax because then we lose
the opacity modifier. We also can't move the opacity modifier to the
candidate itself e.g.: `/50` because we have 2 different variables to
worry about.

In this situation we convert to the modern `theme(…)` syntax itself.

---

**Inside variants:**

Input:
```html
<div class="max-[theme(spacing.20)]:flex"></div>
```

Output:
```html
<div class="max-[theme(--spacing-20)]:flex"></div>
```

Unfortunately we can't convert to `var(…)` syntax reliably because in
some cases (like the one above) the value will be used inside of an
`@media (…)` query and CSS doesn't support that at the time of writing
this PR.

So to be safe, we will not try to convert `theme(…)` to `var(…)` in
variants, but we will only upgrade the `theme(…)` call itself to modern
syntax.
  • Loading branch information
RobinMalfait authored Oct 16, 2024
1 parent bf17991 commit 8dc343d
Show file tree
Hide file tree
Showing 6 changed files with 403 additions and 3 deletions.
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) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})

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

0 comments on commit 8dc343d

Please sign in to comment.