Skip to content

Commit 5bf2efb

Browse files
committed
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; } ```
1 parent d59f1b3 commit 5bf2efb

File tree

7 files changed

+109
-0
lines changed

7 files changed

+109
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- _Upgrade (experimental)_: Migrate `plugins` with options to CSS ([#14700](https://github.com/tailwindlabs/tailwindcss/pull/14700))
1313
- _Upgrade (experimental)_: Allow JS configuration files with `corePlugins` options to be migrated to CSS ([#14742](https://github.com/tailwindlabs/tailwindcss/pull/14742))
14+
- _Upgrade (experimental)_: Migrate `@variants` and `@responsive` directives ([#14748](https://github.com/tailwindlabs/tailwindcss/pull/14748))
1415

1516
### Fixed
1617

integrations/upgrade/index.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ test(
2626
@tailwind base;
2727
@tailwind components;
2828
@tailwind utilities;
29+
30+
@variants hover, focus {
31+
.foo {
32+
color: red;
33+
}
34+
}
2935
`,
3036
},
3137
},
@@ -40,6 +46,10 @@ test(
4046
4147
--- ./src/input.css ---
4248
@import 'tailwindcss';
49+
50+
@utility foo {
51+
color: red;
52+
}
4353
"
4454
`)
4555

packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,3 +400,19 @@ it('should drop `@tailwind variants;`', async () => {
400400
`),
401401
).toEqual('')
402402
})
403+
404+
it('should replace `@responsive` with its children', async () => {
405+
expect(
406+
await migrate(css`
407+
@responsive {
408+
.foo {
409+
color: red;
410+
}
411+
}
412+
`),
413+
).toMatchInlineSnapshot(`
414+
".foo {
415+
color: red;
416+
}"
417+
`)
418+
})

packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,18 @@ export function migrateTailwindDirectives(options: { newPrefix: string | null })
5454
) {
5555
node.remove()
5656
}
57+
58+
// Replace Tailwind CSS v2 directives that still worked in v3.
59+
else if (node.name === 'responsive') {
60+
if (node.nodes) {
61+
for (let child of node.nodes) {
62+
child.raws.tailwind_pretty = true
63+
}
64+
node.replaceWith(node.nodes)
65+
} else {
66+
node.remove()
67+
}
68+
}
5769
})
5870

5971
// Insert default import if all directives are present
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import dedent from 'dedent'
2+
import postcss from 'postcss'
3+
import { expect, it } from 'vitest'
4+
import { formatNodes } from './format-nodes'
5+
import { migrateVariantsDirective } from './migrate-variants-directive'
6+
7+
const css = dedent
8+
9+
function migrate(input: string) {
10+
return postcss()
11+
.use(migrateVariantsDirective())
12+
.use(formatNodes())
13+
.process(input, { from: expect.getState().testPath })
14+
.then((result) => result.css)
15+
}
16+
17+
it('should replace `@variants` with `@layer utilities`', async () => {
18+
expect(
19+
await migrate(css`
20+
@variants hover, focus {
21+
.foo {
22+
color: red;
23+
}
24+
}
25+
`),
26+
).toMatchInlineSnapshot(`
27+
"@layer utilities {
28+
.foo {
29+
color: red;
30+
}
31+
}"
32+
`)
33+
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { type Plugin, type Root } from 'postcss'
2+
3+
export function migrateVariantsDirective(): Plugin {
4+
function migrate(root: Root) {
5+
root.walkAtRules('variants', (node) => {
6+
// Migrate `@variants` to `@utility` because `@variants` make the classes
7+
// an actual utility.
8+
// ```css
9+
// @variants hover {
10+
// .foo {}
11+
// }
12+
// ```
13+
//
14+
// Means that you can do this in your HTML:
15+
// ```html
16+
// <div class="focus:foo"></div>
17+
// ```
18+
//
19+
// Notice the `focus:`, even though we _only_ configured the `hover`
20+
// variant.
21+
//
22+
// This means that we can convert it to an `@layer utilities` rule. Later,
23+
// this will get converted to an `@utility` rule.
24+
if (node.name === 'variants') {
25+
node.name = 'layer'
26+
node.params = 'utilities'
27+
}
28+
})
29+
}
30+
31+
return {
32+
postcssPlugin: '@tailwindcss/upgrade/migrate-variants-directive',
33+
OnceExit: migrate,
34+
}
35+
}

packages/@tailwindcss-upgrade/src/migrate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { migrateMediaScreen } from './codemods/migrate-media-screen'
1111
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
1212
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
1313
import { migrateThemeToVar } from './codemods/migrate-theme-to-var'
14+
import { migrateVariantsDirective } from './codemods/migrate-variants-directive'
1415
import type { JSConfigMigration } from './migrate-js-config'
1516
import { Stylesheet, type StylesheetConnection, type StylesheetId } from './stylesheet'
1617
import { resolveCssId } from './utils/resolve'
@@ -38,6 +39,7 @@ export async function migrateContents(
3839
.use(migrateAtApply(options))
3940
.use(migrateThemeToVar(options))
4041
.use(migrateMediaScreen(options))
42+
.use(migrateVariantsDirective())
4143
.use(migrateAtLayerUtilities(stylesheet))
4244
.use(migrateMissingLayers())
4345
.use(migrateTailwindDirectives(options))

0 commit comments

Comments
 (0)