Add codemod for migrating @variants and @responsive directives (#14748)

This PR migrates the `@variants` and `@responsive` directives.

In Tailwind CSS v2, these were used to generate certain variants of responsive variants for the give classes. In Tailwind CSS v3, these still worked but were implemented as a no-op such that these directives don't end up in your final CSS.

In Tailwind CSS v4, these don't exist at all anymore, so we can safely get rid of them by replacing them with their contents.

Input:
```css
@variants hover, focus {
  .foo {
    color: red;
  }
}

@responsive {
  .bar {
    color: blue;
  }
}
```

Output:
```css
.foo {
  color: red;
}

.bar {
  color: blue;
}
```
This commit is contained in:
RobinMalfait 2024-10-22 16:09:53 +00:00
parent d59f1b3e5d
commit 5bf2efb521
7 changed files with 109 additions and 0 deletions

View file

@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700))
- _Upgrade (experimental)_: Allow JS configuration files with `corePlugins` options to be migrated to CSS ([#14742](https://github.com/tailwindlabs/tailwindcss/pull/14742))
- _Upgrade (experimental)_: Migrate `@variants` and `@responsive` directives ([#14748](https://github.com/tailwindlabs/tailwindcss/pull/14748))
### Fixed

View file

@ -26,6 +26,12 @@ test(
@tailwind base;
@tailwind components;
@tailwind utilities;
@variants hover, focus {
.foo {
color: red;
}
}
`,
},
},
@ -40,6 +46,10 @@ test(
--- ./src/input.css ---
@import 'tailwindcss';
@utility foo {
color: red;
}
"
`)

View file

@ -400,3 +400,19 @@ it('should drop `@tailwind variants;`', async () => {
`),
).toEqual('')
})
it('should replace `@responsive` with its children', async () => {
expect(
await migrate(css`
@responsive {
.foo {
color: red;
}
}
`),
).toMatchInlineSnapshot(`
".foo {
color: red;
}"
`)
})

View file

@ -54,6 +54,18 @@ export function migrateTailwindDirectives(options: { newPrefix: string | null })
) {
node.remove()
}
// Replace Tailwind CSS v2 directives that still worked in v3.
else if (node.name === 'responsive') {
if (node.nodes) {
for (let child of node.nodes) {
child.raws.tailwind_pretty = true
}
node.replaceWith(node.nodes)
} else {
node.remove()
}
}
})
// Insert default import if all directives are present

View file

@ -0,0 +1,33 @@
import dedent from 'dedent'
import postcss from 'postcss'
import { expect, it } from 'vitest'
import { formatNodes } from './format-nodes'
import { migrateVariantsDirective } from './migrate-variants-directive'
const css = dedent
function migrate(input: string) {
return postcss()
.use(migrateVariantsDirective())
.use(formatNodes())
.process(input, { from: expect.getState().testPath })
.then((result) => result.css)
}
it('should replace `@variants` with `@layer utilities`', async () => {
expect(
await migrate(css`
@variants hover, focus {
.foo {
color: red;
}
}
`),
).toMatchInlineSnapshot(`
"@layer utilities {
.foo {
color: red;
}
}"
`)
})

View file

@ -0,0 +1,35 @@
import { type Plugin, type Root } from 'postcss'
export function migrateVariantsDirective(): Plugin {
function migrate(root: Root) {
root.walkAtRules('variants', (node) => {
// Migrate `@variants` to `@utility` because `@variants` make the classes
// an actual utility.
// ```css
// @variants hover {
// .foo {}
// }
// ```
//
// Means that you can do this in your HTML:
// ```html
// <div class="focus:foo"></div>
// ```
//
// Notice the `focus:`, even though we _only_ configured the `hover`
// variant.
//
// This means that we can convert it to an `@layer utilities` rule. Later,
// this will get converted to an `@utility` rule.
if (node.name === 'variants') {
node.name = 'layer'
node.params = 'utilities'
}
})
}
return {
postcssPlugin: '@tailwindcss/upgrade/migrate-variants-directive',
OnceExit: migrate,
}
}

View file

@ -11,6 +11,7 @@ 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 { migrateVariantsDirective } from './codemods/migrate-variants-directive'
import type { JSConfigMigration } from './migrate-js-config'
import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet'
import { resolveCssId } from './utils/resolve'
@ -38,6 +39,7 @@ export async function migrateContents(
.use(migrateAtApply(options))
.use(migrateThemeToVar(options))
.use(migrateMediaScreen(options))
.use(migrateVariantsDirective())
.use(migrateAtLayerUtilities(stylesheet))
.use(migrateMissingLayers())
.use(migrateTailwindDirectives(options))