From 3ae22f1f9d8dd6d8936d701c97a447dd9bfc19f1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 10 Oct 2024 15:06:53 +0200 Subject: [PATCH] =?UTF-8?q?Migrate=20`@media=20screen(=E2=80=A6)`=20(#1460?= =?UTF-8?q?3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds a codemod that migrates the `@media screen(…)` to the properly expanded `@media (…)` syntax. ```css @media screen(md) { .foo { color: red; } } ``` Will be converted to: ```css @media (width >= 48rem) { .foo { color: red; } } ``` If you happen to have custom screens (even complex ones), the screen will be converted to a custom media query. ```css @media screen(foo) { .foo { color: red; } } ``` With a custom `tailwind.config.js` file with a config like this: ```js module.exports = { // … theme: { screens: { foo: { min: '100px', max: '123px' }, }, } } ``` Then the codemod will convert it to: ```css @media (123px >= width >= 100px) { .foo { color: red; } } ``` --- CHANGELOG.md | 1 + .../src/codemods/migrate-media-screen.test.ts | 177 ++++++++++++++++++ .../src/codemods/migrate-media-screen.ts | 44 +++++ packages/@tailwindcss-upgrade/src/migrate.ts | 2 + 4 files changed, 224 insertions(+) create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts create mode 100644 packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 067c06bf59a1..322f5eaea3e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pass options when using `addComponents` and `matchComponents` ([#14590](https://github.com/tailwindlabs/tailwindcss/pull/14590)) - _Upgrade (experimental)_: Ensure CSS before a layer stays unlayered when running codemods ([#14596](https://github.com/tailwindlabs/tailwindcss/pull/14596)) - _Upgrade (experimental)_: Resolve issues where some prefixed candidates were not properly migrated ([#14600](https://github.com/tailwindlabs/tailwindcss/pull/14600)) +- _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603)) ## [4.0.0-alpha.26] - 2024-10-03 diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts new file mode 100644 index 000000000000..b7370d4d1953 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.test.ts @@ -0,0 +1,177 @@ +import { __unstable__loadDesignSystem } from '@tailwindcss/node' +import dedent from 'dedent' +import postcss from 'postcss' +import { expect, it } from 'vitest' +import type { UserConfig } from '../../../tailwindcss/src/compat/config/types' +import { formatNodes } from './format-nodes' +import { migrateMediaScreen } from './migrate-media-screen' + +const css = dedent + +async function migrate(input: string, userConfig: UserConfig = {}) { + return postcss() + .use( + migrateMediaScreen({ + designSystem: await __unstable__loadDesignSystem(`@import 'tailwindcss';`, { + base: __dirname, + }), + userConfig, + }), + ) + .use(formatNodes()) + .process(input, { from: expect.getState().testPath }) + .then((result) => result.css) +} + +it('should migrate a built-in breakpoint', async () => { + expect( + await migrate(css` + @media screen(md) { + .foo { + color: red; + } + } + `), + ).toMatchInlineSnapshot(` + "@media (width >= theme(--breakpoint-md)) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a custom min-width screen (string)', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: '123px', + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media (width >= theme(--breakpoint-foo)) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a custom min-width screen (object)', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: { min: '123px' }, + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media (width >= theme(--breakpoint-foo)) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a custom max-width screen', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: { max: '123px' }, + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media (123px >= width) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a custom min and max-width screen', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: { min: '100px', max: '123px' }, + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media (123px >= width >= 100px) { + .foo { + color: red; + } + }" + `) +}) + +it('should migrate a raw media query', async () => { + expect( + await migrate( + css` + @media screen(foo) { + .foo { + color: red; + } + } + `, + { + theme: { + screens: { + foo: { raw: 'only screen and (min-width: 123px)' }, + }, + }, + }, + ), + ).toMatchInlineSnapshot(` + "@media only screen and (min-width: 123px) { + .foo { + color: red; + } + }" + `) +}) diff --git a/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts new file mode 100644 index 000000000000..d8343984a890 --- /dev/null +++ b/packages/@tailwindcss-upgrade/src/codemods/migrate-media-screen.ts @@ -0,0 +1,44 @@ +import { type Plugin, type Root } from 'postcss' +import type { Config } from 'tailwindcss' +import { resolveConfig } from '../../../tailwindcss/src/compat/config/resolve-config' +import { buildMediaQuery } from '../../../tailwindcss/src/compat/screens-config' +import type { DesignSystem } from '../../../tailwindcss/src/design-system' +import { DefaultMap } from '../../../tailwindcss/src/utils/default-map' + +export function migrateMediaScreen({ + designSystem, + userConfig, +}: { + designSystem?: DesignSystem + userConfig?: Config +} = {}): Plugin { + function migrate(root: Root) { + if (!designSystem || !userConfig) return + + let resolvedUserConfig = resolveConfig(designSystem, [{ base: '', config: userConfig }]) + let screens = resolvedUserConfig?.theme?.screens || {} + + let mediaQueries = new DefaultMap((name) => { + let value = designSystem?.resolveThemeValue(`--breakpoint-${name}`) ?? screens?.[name] + if (typeof value === 'string') return `(width >= theme(--breakpoint-${name}))` + return value ? buildMediaQuery(value) : null + }) + + root.walkAtRules((rule) => { + if (rule.name !== 'media') return + + let screen = rule.params.match(/screen\(([^)]+)\)/) + if (!screen) return + + let value = mediaQueries.get(screen[1]) + if (!value) return + + rule.params = value + }) + } + + return { + postcssPlugin: '@tailwindcss/upgrade/migrate-media-screen', + OnceExit: migrate, + } +} diff --git a/packages/@tailwindcss-upgrade/src/migrate.ts b/packages/@tailwindcss-upgrade/src/migrate.ts index 76990936b549..a47289b09f0e 100644 --- a/packages/@tailwindcss-upgrade/src/migrate.ts +++ b/packages/@tailwindcss-upgrade/src/migrate.ts @@ -6,6 +6,7 @@ import type { DesignSystem } from '../../tailwindcss/src/design-system' import { formatNodes } from './codemods/format-nodes' import { migrateAtApply } from './codemods/migrate-at-apply' import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities' +import { migrateMediaScreen } from './codemods/migrate-media-screen' import { migrateMissingLayers } from './codemods/migrate-missing-layers' import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives' @@ -18,6 +19,7 @@ export interface MigrateOptions { export async function migrateContents(contents: string, options: MigrateOptions, file?: string) { return postcss() .use(migrateAtApply(options)) + .use(migrateMediaScreen(options)) .use(migrateAtLayerUtilities()) .use(migrateMissingLayers()) .use(migrateTailwindDirectives(options))