diff --git a/CHANGELOG.md b/CHANGELOG.md index 437259146..1d0b4831f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/integrations/upgrade/index.test.ts b/integrations/upgrade/index.test.ts index 80005c088..ceec310ae 100644 --- a/integrations/upgrade/index.test.ts +++ b/integrations/upgrade/index.test.ts @@ -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; + } " `) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts index ba2d9ea9d..7c74b1ed6 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.test.ts @@ -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; + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts index 0f7143017..d3d74a65e 100644 --- a/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-tailwind-directives.ts @@ -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 diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts new file mode 100644 index 000000000..e18a161e6 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.test.ts @@ -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; + } + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.ts new file mode 100644 index 000000000..1592c0059 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-variants-directive.ts @@ -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 + //
+ // ``` + // + // 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, + } +} diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index 1b72afc6f..4220904c9 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -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))