Migrate theme(…) to var(…) in CSS (#14695)

This PR is a follow up from https://github.com/tailwindlabs/tailwindcss/pull/14664 migrates all the `theme(…)` calls in your CSS to `var(…)` if we can.

In at-rules, like `@media` we can't use `var(…)` so we have to use the modern version of `theme(…)`.

In declarations, we can convert to `var(…)` unless there is a modifier used in the `theme(…)` function, then we can only convert to the new `theme(…)` syntax without dot notation.

Input:
```css
@media theme(spacing.4) {
  .foo {
    background-color: theme(colors.red.900);
    color: theme(colors.red.900 / 75%); /* With spaces around the `/` */
    border-color: theme(colors.red.200/75%); /* Without spaces around the `/` */
  }
}
```

Output:
```css
@media theme(--spacing-4) {
/*     ^^^^^^^^^^^^^^^^^^     Use `theme(…)` since `var(…)` is invalid in this position*/
  .foo {
    background-color: var(--color-red-900); /* Converted to var(…) */
    color: theme(--color-red-900 / 75%); /* Convert to modern theme(…) */
    border-color: theme(--color-red-200 / 75%); /* Convert to modern theme(…) — pretty printed*/
  }
}
```
This commit is contained in:
Robin Malfait 2024-10-17 11:50:27 +02:00 committed by GitHub
parent aff858a3e6
commit 4a4be27dc1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 85 additions and 4 deletions

View File

@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add first draft of new wide-gamut color palette ([#14693](https://github.com/tailwindlabs/tailwindcss/pull/14693))
- _Upgrade (experimental)_: Migrate `theme(…)` calls in classes to `var(…)` or to the modern `theme(…)` syntax ([#14664](https://github.com/tailwindlabs/tailwindcss/pull/14664))
- _Upgrade (experimental)_: Migrate `theme(…)` calls to `var(…)` or to the modern `theme(…)` syntax ([#14664](https://github.com/tailwindlabs/tailwindcss/pull/14664), [#14695](https://github.com/tailwindlabs/tailwindcss/pull/14695))
### Fixed

View File

@ -0,0 +1,44 @@
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
import dedent from 'dedent'
import postcss from 'postcss'
import { expect, it } from 'vitest'
import { formatNodes } from './format-nodes'
import { migrateThemeToVar } from './migrate-theme-to-var'
const css = dedent
async function migrate(input: string) {
return postcss()
.use(
migrateThemeToVar({
designSystem: await __unstable__loadDesignSystem(`@import 'tailwindcss';`, {
base: __dirname,
}),
}),
)
.use(formatNodes())
.process(input, { from: expect.getState().testPath })
.then((result) => result.css)
}
it('should migrate `theme(…)` to `var(…)`', async () => {
expect(
await migrate(css`
@media theme(spacing.4) {
.foo {
background-color: theme(colors.red.900);
color: theme(colors.red.900 / 75%);
border-color: theme(colors.red.200/75%);
}
}
`),
).toMatchInlineSnapshot(`
"@media theme(--spacing-4) {
.foo {
background-color: var(--color-red-900);
color: theme(--color-red-900 / 75%);
border-color: theme(--color-red-200 / 75%);
}
}"
`)
})

View File

@ -0,0 +1,34 @@
import { type Plugin } from 'postcss'
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
import { Convert, createConverter } from '../template/codemods/theme-to-var'
export function migrateThemeToVar({
designSystem,
}: {
designSystem?: DesignSystem
} = {}): Plugin {
return {
postcssPlugin: '@tailwindcss/upgrade/migrate-theme-to-var',
OnceExit(root) {
if (!designSystem) return
let convert = createConverter(designSystem, { prettyPrint: true })
root.walkDecls((decl) => {
let [newValue] = convert(decl.value)
decl.value = newValue
})
root.walkAtRules((atRule) => {
if (
atRule.name === 'media' ||
atRule.name === 'custom-media' ||
atRule.name === 'container' ||
atRule.name === 'supports'
) {
let [newValue] = convert(atRule.params, Convert.MigrateThemeOnly)
atRule.params = newValue
}
})
},
}
}

View File

@ -10,6 +10,7 @@ import { migrateConfig } from './codemods/migrate-config'
import { migrateMediaScreen } from './codemods/migrate-media-screen'
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
import { migrateThemeToVar } from './codemods/migrate-theme-to-var'
import type { JSConfigMigration } from './migrate-js-config'
import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet'
import { resolveCssId } from './utils/resolve'
@ -35,6 +36,7 @@ export async function migrateContents(
return postcss()
.use(migrateAtApply(options))
.use(migrateThemeToVar(options))
.use(migrateMediaScreen(options))
.use(migrateAtLayerUtilities(stylesheet))
.use(migrateMissingLayers())

View File

@ -80,7 +80,7 @@ export function themeToVar(
return rawCandidate
}
export function createConverter(designSystem: DesignSystem) {
export function createConverter(designSystem: DesignSystem, { prettyPrint = false } = {}) {
function convert(input: string, options = Convert.All): [string, CandidateModifier | null] {
let ast = ValueParser.parse(input)
@ -110,7 +110,7 @@ export function createConverter(designSystem: DesignSystem) {
}
// If we see a `/`, we have a modifier
else if (child.kind === 'separator' && child.value === '/') {
else if (child.kind === 'separator' && child.value.trim() === '/') {
themeModifierCount += 1
return ValueParser.ValueWalkAction.Stop
}
@ -211,7 +211,8 @@ export function createConverter(designSystem: DesignSystem) {
let variable = pathToVariableName(path)
if (!variable) return null
let modifier = parts.length > 0 ? `/${parts.join('/')}` : ''
let modifier =
parts.length > 0 ? (prettyPrint ? ` / ${parts.join(' / ')}` : `/${parts.join('/')}`) : ''
return fallback ? `theme(${variable}${modifier}, ${fallback})` : `theme(${variable}${modifier})`
}