Skip to content

Commit 3ae22f1

Browse files
authored
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; } } ```
1 parent 958bfc9 commit 3ae22f1

File tree

4 files changed

+224
-0
lines changed

4 files changed

+224
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
- Pass options when using `addComponents` and `matchComponents` ([#14590](https://github.com/tailwindlabs/tailwindcss/pull/14590))
2626
- _Upgrade (experimental)_: Ensure CSS before a layer stays unlayered when running codemods ([#14596](https://github.com/tailwindlabs/tailwindcss/pull/14596))
2727
- _Upgrade (experimental)_: Resolve issues where some prefixed candidates were not properly migrated ([#14600](https://github.com/tailwindlabs/tailwindcss/pull/14600))
28+
- _Upgrade (experimental)_: Migrate `@media screen(…)` when running codemods ([#14603](https://github.com/tailwindlabs/tailwindcss/pull/14603))
2829

2930
## [4.0.0-alpha.26] - 2024-10-03
3031

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
2+
import dedent from 'dedent'
3+
import postcss from 'postcss'
4+
import { expect, it } from 'vitest'
5+
import type { UserConfig } from '../../../tailwindcss/src/compat/config/types'
6+
import { formatNodes } from './format-nodes'
7+
import { migrateMediaScreen } from './migrate-media-screen'
8+
9+
const css = dedent
10+
11+
async function migrate(input: string, userConfig: UserConfig = {}) {
12+
return postcss()
13+
.use(
14+
migrateMediaScreen({
15+
designSystem: await __unstable__loadDesignSystem(`@import 'tailwindcss';`, {
16+
base: __dirname,
17+
}),
18+
userConfig,
19+
}),
20+
)
21+
.use(formatNodes())
22+
.process(input, { from: expect.getState().testPath })
23+
.then((result) => result.css)
24+
}
25+
26+
it('should migrate a built-in breakpoint', async () => {
27+
expect(
28+
await migrate(css`
29+
@media screen(md) {
30+
.foo {
31+
color: red;
32+
}
33+
}
34+
`),
35+
).toMatchInlineSnapshot(`
36+
"@media (width >= theme(--breakpoint-md)) {
37+
.foo {
38+
color: red;
39+
}
40+
}"
41+
`)
42+
})
43+
44+
it('should migrate a custom min-width screen (string)', async () => {
45+
expect(
46+
await migrate(
47+
css`
48+
@media screen(foo) {
49+
.foo {
50+
color: red;
51+
}
52+
}
53+
`,
54+
{
55+
theme: {
56+
screens: {
57+
foo: '123px',
58+
},
59+
},
60+
},
61+
),
62+
).toMatchInlineSnapshot(`
63+
"@media (width >= theme(--breakpoint-foo)) {
64+
.foo {
65+
color: red;
66+
}
67+
}"
68+
`)
69+
})
70+
71+
it('should migrate a custom min-width screen (object)', async () => {
72+
expect(
73+
await migrate(
74+
css`
75+
@media screen(foo) {
76+
.foo {
77+
color: red;
78+
}
79+
}
80+
`,
81+
{
82+
theme: {
83+
screens: {
84+
foo: { min: '123px' },
85+
},
86+
},
87+
},
88+
),
89+
).toMatchInlineSnapshot(`
90+
"@media (width >= theme(--breakpoint-foo)) {
91+
.foo {
92+
color: red;
93+
}
94+
}"
95+
`)
96+
})
97+
98+
it('should migrate a custom max-width screen', async () => {
99+
expect(
100+
await migrate(
101+
css`
102+
@media screen(foo) {
103+
.foo {
104+
color: red;
105+
}
106+
}
107+
`,
108+
{
109+
theme: {
110+
screens: {
111+
foo: { max: '123px' },
112+
},
113+
},
114+
},
115+
),
116+
).toMatchInlineSnapshot(`
117+
"@media (123px >= width) {
118+
.foo {
119+
color: red;
120+
}
121+
}"
122+
`)
123+
})
124+
125+
it('should migrate a custom min and max-width screen', async () => {
126+
expect(
127+
await migrate(
128+
css`
129+
@media screen(foo) {
130+
.foo {
131+
color: red;
132+
}
133+
}
134+
`,
135+
{
136+
theme: {
137+
screens: {
138+
foo: { min: '100px', max: '123px' },
139+
},
140+
},
141+
},
142+
),
143+
).toMatchInlineSnapshot(`
144+
"@media (123px >= width >= 100px) {
145+
.foo {
146+
color: red;
147+
}
148+
}"
149+
`)
150+
})
151+
152+
it('should migrate a raw media query', async () => {
153+
expect(
154+
await migrate(
155+
css`
156+
@media screen(foo) {
157+
.foo {
158+
color: red;
159+
}
160+
}
161+
`,
162+
{
163+
theme: {
164+
screens: {
165+
foo: { raw: 'only screen and (min-width: 123px)' },
166+
},
167+
},
168+
},
169+
),
170+
).toMatchInlineSnapshot(`
171+
"@media only screen and (min-width: 123px) {
172+
.foo {
173+
color: red;
174+
}
175+
}"
176+
`)
177+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { type Plugin, type Root } from 'postcss'
2+
import type { Config } from 'tailwindcss'
3+
import { resolveConfig } from '../../../tailwindcss/src/compat/config/resolve-config'
4+
import { buildMediaQuery } from '../../../tailwindcss/src/compat/screens-config'
5+
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
6+
import { DefaultMap } from '../../../tailwindcss/src/utils/default-map'
7+
8+
export function migrateMediaScreen({
9+
designSystem,
10+
userConfig,
11+
}: {
12+
designSystem?: DesignSystem
13+
userConfig?: Config
14+
} = {}): Plugin {
15+
function migrate(root: Root) {
16+
if (!designSystem || !userConfig) return
17+
18+
let resolvedUserConfig = resolveConfig(designSystem, [{ base: '', config: userConfig }])
19+
let screens = resolvedUserConfig?.theme?.screens || {}
20+
21+
let mediaQueries = new DefaultMap<string, string | null>((name) => {
22+
let value = designSystem?.resolveThemeValue(`--breakpoint-${name}`) ?? screens?.[name]
23+
if (typeof value === 'string') return `(width >= theme(--breakpoint-${name}))`
24+
return value ? buildMediaQuery(value) : null
25+
})
26+
27+
root.walkAtRules((rule) => {
28+
if (rule.name !== 'media') return
29+
30+
let screen = rule.params.match(/screen\(([^)]+)\)/)
31+
if (!screen) return
32+
33+
let value = mediaQueries.get(screen[1])
34+
if (!value) return
35+
36+
rule.params = value
37+
})
38+
}
39+
40+
return {
41+
postcssPlugin: '@tailwindcss/upgrade/migrate-media-screen',
42+
OnceExit: migrate,
43+
}
44+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { DesignSystem } from '../../tailwindcss/src/design-system'
66
import { formatNodes } from './codemods/format-nodes'
77
import { migrateAtApply } from './codemods/migrate-at-apply'
88
import { migrateAtLayerUtilities } from './codemods/migrate-at-layer-utilities'
9+
import { migrateMediaScreen } from './codemods/migrate-media-screen'
910
import { migrateMissingLayers } from './codemods/migrate-missing-layers'
1011
import { migrateTailwindDirectives } from './codemods/migrate-tailwind-directives'
1112

@@ -18,6 +19,7 @@ export interface MigrateOptions {
1819
export async function migrateContents(contents: string, options: MigrateOptions, file?: string) {
1920
return postcss()
2021
.use(migrateAtApply(options))
22+
.use(migrateMediaScreen(options))
2123
.use(migrateAtLayerUtilities())
2224
.use(migrateMissingLayers())
2325
.use(migrateTailwindDirectives(options))

0 commit comments

Comments
 (0)