Migrate keyframes from JS to CSS (#14666)

This PR adds support for rewriting JS theme config `keyframes` to CSS as
part of the JS config to CSS migration.

Example:

```ts
import { type Config } from 'tailwindcss'

module.exports = {
  theme: {
    extend: {
      keyframes: {
        'spin-clockwise': {
          '0%': { transform: 'rotate(0deg)' },
          '100%': { transform: 'rotate(360deg)' },
        },
        'spin-counterclockwise': {
          '0%': { transform: 'rotate(0deg)' },
          '100%': { transform: 'rotate(-360deg)' },
        },
      },
      animation: {
        'spin-clockwise': 'spin-clockwise 1s linear infinite',
        'spin-counterclockwise': 'spin-counterclockwise 1s linear infinite',
      },
    },
  },
} satisfies Config
```

Will be printed as:

```css
@theme {
  --animate-spin-clockwise: spin-clockwise 1s linear infinite;
  --animate-spin-counterclockwise: spin-counterclockwise 1s linear infinite;

  @keyframes spin-clockwise {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
  @keyframes spin-counterclockwise {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(-360deg);
    }
  }
}
```
This commit is contained in:
Philipp Spiess 2024-10-15 11:21:19 +02:00 • committed by GitHub
parent 468cb5e99e
commit 782bc26135
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 64 additions and 5 deletions

View file

@ -16,7 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Migrate v3 PostCSS setups to v4 in some cases ([#14612](https://github.com/tailwindlabs/tailwindcss/pull/14612))
- _Upgrade (experimental)_: Automatically discover JavaScript config files ([#14597](https://github.com/tailwindlabs/tailwindcss/pull/14597))
- _Upgrade (experimental)_: Migrate legacy classes to the v4 alternative ([#14643](https://github.com/tailwindlabs/tailwindcss/pull/14643))
- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639), [#14650](https://github.com/tailwindlabs/tailwindcss/pull/14650), [#14648](https://github.com/tailwindlabs/tailwindcss/pull/14648))
- _Upgrade (experimental)_: Migrate static JS configurations to CSS ([#14639](https://github.com/tailwindlabs/tailwindcss/pull/14639), [#14650](https://github.com/tailwindlabs/tailwindcss/pull/14650), [#14648](https://github.com/tailwindlabs/tailwindcss/pull/14648), [#14666](https://github.com/tailwindlabs/tailwindcss/pull/14666))
- _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603))
- _Upgrade (experimental)_: Inject `@config "…"` when a `tailwind.config.{js,ts,…}` is detected ([#14635](https://github.com/tailwindlabs/tailwindcss/pull/14635))
- _Upgrade (experimental)_: Migrate `aria-*`, `data-*`, and `supports-*` variants from arbitrary values to bare values ([#14644](https://github.com/tailwindlabs/tailwindcss/pull/14644))

View file

@ -48,6 +48,20 @@ test(
borderRadius: {
'4xl': '2rem',
},
keyframes: {
'spin-clockwise': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
},
'spin-counterclockwise': {
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(-360deg)' },
},
},
animation: {
'spin-clockwise': 'spin-clockwise 1s linear infinite',
'spin-counterclockwise': 'spin-counterclockwise 1s linear infinite',
},
},
},
plugins: [],
@ -91,9 +105,30 @@ test(
--font-size-base--line-height: 2rem;
--font-family-sans: Inter, system-ui, sans-serif;
--font-family-display: Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-family-display: Cabinet Grotesk, ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--radius-4xl: 2rem;
--animate-spin-clockwise: spin-clockwise 1s linear infinite;
--animate-spin-counterclockwise: spin-counterclockwise 1s linear infinite;
@keyframes spin-clockwise {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes spin-counterclockwise {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-360deg);
}
}
}
"
`)

View file

@ -30,7 +30,6 @@ export function migrateConfig(
if (!sheet.file) return
let cssConfig = new AtRule()
cssConfig.raws.tailwind_pretty = true
if (jsConfigMigration === null) {
// Skip if there is already a `@config` directive
@ -85,6 +84,10 @@ export function migrateConfig(
return WalkAction.Skip
})
for (let node of cssConfig?.nodes ?? []) {
node.raws.tailwind_pretty = true
}
if (!locationNode) {
root.prepend(cssConfig.nodes)
} else if (locationNode.name === 'import') {

View file

@ -4,10 +4,12 @@ import type { Config } from 'tailwindcss'
import defaultTheme from 'tailwindcss/defaultTheme'
import { fileURLToPath } from 'url'
import { loadModule } from '../../@tailwindcss-node/src/compile'
import { toCss, type AstNode } from '../../tailwindcss/src/ast'
import {
keyPathToCssProperty,
themeableValues,
} from '../../tailwindcss/src/compat/apply-config-to-theme'
import { applyKeyframesToAst } from '../../tailwindcss/src/compat/apply-keyframes-to-ast'
import { deepMerge } from '../../tailwindcss/src/compat/config/deep-merge'
import { mergeThemeExtension } from '../../tailwindcss/src/compat/config/resolve-config'
import type { ThemeConfig } from '../../tailwindcss/src/compat/config/types'
@ -89,7 +91,11 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise<
}
}
let themeValues = deepMerge({}, [overwriteTheme, extendTheme], mergeThemeExtension)
let themeValues: Record<string, Record<string, unknown>> = deepMerge(
{},
[overwriteTheme, extendTheme],
mergeThemeExtension,
)
let prevSectionKey = ''
@ -99,6 +105,10 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise<
if (typeof value !== 'string' && typeof value !== 'number') {
continue
}
if (key[0] === 'keyframes') {
continue
}
containsThemeKeys = true
let sectionKey = createSectionKey(key)
@ -115,6 +125,11 @@ async function migrateTheme(unresolvedConfig: Config & { theme: any }): Promise<
css += ` --${keyPathToCssProperty(key)}: ${value};\n`
}
if ('keyframes' in themeValues) {
containsThemeKeys = true
css += '\n' + keyframesToCss(themeValues.keyframes)
}
if (!containsThemeKeys) {
return null
}
@ -232,3 +247,9 @@ function onlyUsesAllowedTopLevelKeys(theme: ThemeConfig): boolean {
}
return true
}
function keyframesToCss(keyframes: Record<string, unknown>): string {
let ast: AstNode[] = []
applyKeyframesToAst(ast, { theme: { keyframes } })
return toCss(ast).trim() + '\n'
}

View file

@ -2,7 +2,7 @@ import { rule, type AstNode } from '../ast'
import type { ResolvedConfig } from './config/types'
import { objectToAst } from './plugin-api'
export function applyKeyframesToAst(ast: AstNode[], { theme }: ResolvedConfig) {
export function applyKeyframesToAst(ast: AstNode[], { theme }: Pick<ResolvedConfig, 'theme'>) {
if ('keyframes' in theme) {
for (let [name, keyframe] of Object.entries(theme.keyframes)) {
ast.push(rule(`@keyframes ${name}`, objectToAst(keyframe as any)))