Migrate @media screen(…) (#14603)

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;
  }
}
```
This commit is contained in:
Robin Malfait 2024-10-10 15:06:53 +02:00 • committed by GitHub
parent 958bfc9d89
commit 3ae22f1f9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 224 additions and 0 deletions

View file

@ -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

View file

@ -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;
}
}"
`)
})

View file

@ -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<string, string | null>((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,
}
}

View file

@ -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))