Skip to content

Commit 2ecb943

Browse files
authored
Refactor @media handling (#14765)
This PR is an internal refactor to improve the way we handle at-rules. Essentially we check for the `@` symbol and bail early if that's not the case. We also improve the way `@media` is being handled by first checking for `@media`, and then handle all the params separately. Last but not least, we also inverted the `@theme` check condition such that it is handled the same way we handle everything else.
1 parent 2327e68 commit 2ecb943

File tree

1 file changed

+88
-94
lines changed

1 file changed

+88
-94
lines changed

packages/tailwindcss/src/index.ts

Lines changed: 88 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,17 @@ async function parseCss(
7676

7777
await substituteAtImports(ast, base, loadStylesheet)
7878

79-
// Find all `@theme` declarations
8079
let important: boolean | null = null
8180
let theme = new Theme()
8281
let customVariants: ((designSystem: DesignSystem) => void)[] = []
8382
let customUtilities: ((designSystem: DesignSystem) => void)[] = []
8483
let firstThemeRule: Rule | null = null
8584
let globs: { base: string; pattern: string }[] = []
8685

86+
// Handle at-rules
8787
walk(ast, (node, { parent, replaceWith, context }) => {
8888
if (node.kind !== 'rule') return
89+
if (node.selector[0] !== '@') return
8990

9091
// Collect custom `@utility` at-rules
9192
if (node.selector.startsWith('@utility ')) {
@@ -196,124 +197,117 @@ async function parseCss(
196197
}
197198
}
198199

199-
// Drop instances of `@media theme(…)`
200-
//
201-
// We support `@import "tailwindcss/theme" theme(reference)` as a way to
202-
// import an external theme file as a reference, which becomes `@media
203-
// theme(reference) { … }` when the `@import` is processed.
204-
if (node.selector.startsWith('@media theme(')) {
205-
let themeParams = node.selector.slice(13, -1)
200+
if (node.selector.startsWith('@media ')) {
201+
let params = segment(node.selector.slice(7), ' ')
202+
let unknownParams: string[] = []
206203

207-
walk(node.nodes, (child) => {
208-
if (child.kind !== 'rule') {
209-
throw new Error(
210-
'Files imported with `@import "…" theme(…)` must only contain `@theme` blocks.',
211-
)
212-
}
213-
if (child.selector === '@theme' || child.selector.startsWith('@theme ')) {
214-
child.selector += ' ' + themeParams
215-
return WalkAction.Skip
204+
for (let param of params) {
205+
// Handle `@media theme(…)`
206+
//
207+
// We support `@import "tailwindcss/theme" theme(reference)` as a way to
208+
// import an external theme file as a reference, which becomes `@media
209+
// theme(reference) { … }` when the `@import` is processed.
210+
if (param.startsWith('theme(')) {
211+
let themeParams = param.slice(6, -1)
212+
213+
walk(node.nodes, (child) => {
214+
if (child.kind !== 'rule') {
215+
throw new Error(
216+
'Files imported with `@import "…" theme(…)` must only contain `@theme` blocks.',
217+
)
218+
}
219+
if (child.selector === '@theme' || child.selector.startsWith('@theme ')) {
220+
child.selector += ' ' + themeParams
221+
return WalkAction.Skip
222+
}
223+
})
216224
}
217-
})
218-
replaceWith(node.nodes)
219-
return WalkAction.Skip
220-
}
221225

222-
// Drop instances of `@media prefix(…)`
223-
//
224-
// We support `@import "tailwindcss" prefix(ident)` as a way to
225-
// configure a theme prefix for variables and utilities.
226-
if (node.selector.startsWith('@media prefix(')) {
227-
let themeParams = node.selector.slice(7)
228-
229-
walk(node.nodes, (child) => {
230-
if (child.kind !== 'rule') return
231-
if (child.selector === '@theme' || child.selector.startsWith('@theme ')) {
232-
child.selector += ' ' + themeParams
233-
return WalkAction.Skip
226+
// Handle `@media prefix(…)`
227+
//
228+
// We support `@import "tailwindcss" prefix(ident)` as a way to
229+
// configure a theme prefix for variables and utilities.
230+
else if (param.startsWith('prefix(')) {
231+
let prefix = param.slice(7, -1)
232+
233+
walk(node.nodes, (child) => {
234+
if (child.kind !== 'rule') return
235+
if (child.selector === '@theme' || child.selector.startsWith('@theme ')) {
236+
child.selector += ` prefix(${prefix})`
237+
return WalkAction.Skip
238+
}
239+
})
234240
}
235-
})
236-
replaceWith(node.nodes)
237-
return WalkAction.Skip
238-
}
239-
240-
if (node.selector.startsWith('@media')) {
241-
let features = segment(node.selector.slice(6), ' ')
242-
let shouldReplace = true
243241

244-
for (let i = 0; i < features.length; i++) {
245-
let part = features[i]
242+
// Handle important
243+
else if (param === 'important') {
244+
important = true
245+
}
246246

247-
// Drop instances of `@media important`
248247
//
249-
// We support `@import "tailwindcss" important` to mark all declarations
250-
// in generated utilities as `!important`.
251-
if (part === 'important') {
252-
important = true
253-
shouldReplace = true
254-
features[i] = ''
248+
else {
249+
unknownParams.push(param)
255250
}
256251
}
257252

258-
let remaining = features.filter(Boolean).join(' ')
259-
260-
node.selector = `@media ${remaining}`
261-
262-
if (remaining.trim() === '' && shouldReplace) {
253+
if (unknownParams.length > 0) {
254+
node.selector = `@media ${unknownParams.join(' ')}`
255+
} else if (params.length > 0) {
263256
replaceWith(node.nodes)
264257
}
265258

266259
return WalkAction.Skip
267260
}
268261

269-
if (node.selector !== '@theme' && !node.selector.startsWith('@theme ')) return
262+
// Handle `@theme`
263+
if (node.selector === '@theme' || node.selector.startsWith('@theme ')) {
264+
let [themeOptions, themePrefix] = parseThemeOptions(node.selector)
270265

271-
let [themeOptions, themePrefix] = parseThemeOptions(node.selector)
266+
if (themePrefix) {
267+
if (!IS_VALID_PREFIX.test(themePrefix)) {
268+
throw new Error(
269+
`The prefix "${themePrefix}" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.`,
270+
)
271+
}
272272

273-
if (themePrefix) {
274-
if (!IS_VALID_PREFIX.test(themePrefix)) {
275-
throw new Error(
276-
`The prefix "${themePrefix}" is invalid. Prefixes must be lowercase ASCII letters (a-z) only.`,
277-
)
273+
theme.prefix = themePrefix
278274
}
279275

280-
theme.prefix = themePrefix
281-
}
282-
283-
// Record all custom properties in the `@theme` declaration
284-
walk(node.nodes, (child, { replaceWith }) => {
285-
// Collect `@keyframes` rules to re-insert with theme variables later,
286-
// since the `@theme` rule itself will be removed.
287-
if (child.kind === 'rule' && child.selector.startsWith('@keyframes ')) {
288-
theme.addKeyframes(child)
289-
replaceWith([])
290-
return WalkAction.Skip
291-
}
276+
// Record all custom properties in the `@theme` declaration
277+
walk(node.nodes, (child, { replaceWith }) => {
278+
// Collect `@keyframes` rules to re-insert with theme variables later,
279+
// since the `@theme` rule itself will be removed.
280+
if (child.kind === 'rule' && child.selector.startsWith('@keyframes ')) {
281+
theme.addKeyframes(child)
282+
replaceWith([])
283+
return WalkAction.Skip
284+
}
292285

293-
if (child.kind === 'comment') return
294-
if (child.kind === 'declaration' && child.property.startsWith('--')) {
295-
theme.add(child.property, child.value ?? '', themeOptions)
296-
return
297-
}
286+
if (child.kind === 'comment') return
287+
if (child.kind === 'declaration' && child.property.startsWith('--')) {
288+
theme.add(child.property, child.value ?? '', themeOptions)
289+
return
290+
}
298291

299-
let snippet = toCss([rule(node.selector, [child])])
300-
.split('\n')
301-
.map((line, idx, all) => `${idx === 0 || idx >= all.length - 2 ? ' ' : '>'} ${line}`)
302-
.join('\n')
292+
let snippet = toCss([rule(node.selector, [child])])
293+
.split('\n')
294+
.map((line, idx, all) => `${idx === 0 || idx >= all.length - 2 ? ' ' : '>'} ${line}`)
295+
.join('\n')
303296

304-
throw new Error(
305-
`\`@theme\` blocks must only contain custom properties or \`@keyframes\`.\n\n${snippet}`,
306-
)
307-
})
297+
throw new Error(
298+
`\`@theme\` blocks must only contain custom properties or \`@keyframes\`.\n\n${snippet}`,
299+
)
300+
})
308301

309-
// Keep a reference to the first `@theme` rule to update with the full theme
310-
// later, and delete any other `@theme` rules.
311-
if (!firstThemeRule && !(themeOptions & ThemeOptions.REFERENCE)) {
312-
firstThemeRule = node
313-
} else {
314-
replaceWith([])
302+
// Keep a reference to the first `@theme` rule to update with the full
303+
// theme later, and delete any other `@theme` rules.
304+
if (!firstThemeRule && !(themeOptions & ThemeOptions.REFERENCE)) {
305+
firstThemeRule = node
306+
} else {
307+
replaceWith([])
308+
}
309+
return WalkAction.Skip
315310
}
316-
return WalkAction.Skip
317311
})
318312

319313
let designSystem = buildDesignSystem(theme)

0 commit comments

Comments
 (0)