From 65c401f2894e7b6faa9b03b35edbdcb2870a9ba5 Mon Sep 17 00:00:00 2001 From: cobyfrombrooklyn-bot Date: Sun, 22 Feb 2026 07:45:03 -0500 Subject: [PATCH] Fix CSS minifier dropping parentheses around nested 'or' in @media rules When minifying @media only screen and ((min-width: 10px) or (min-height: 10px)), the outer parentheses around the 'or' condition were being dropped, producing invalid CSS: @media only screen and (min-width:10px)or (min-height:10px). The CSS spec requires 'or' clauses to be nested inside parentheses when used after a media type with 'and'. Without the outer parens, browsers ignore the entire media query. Fix: when printing MQType.AndOrNull, pass mqNeedsParens if the inner query is an MQBinary with 'or' operator, ensuring the grouping parentheses are preserved. Fixes #4395 --- internal/css_printer/css_printer.go | 9 ++++++++- internal/css_printer/css_printer_test.go | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/internal/css_printer/css_printer.go b/internal/css_printer/css_printer.go index e42947ba0a..4c1a184794 100644 --- a/internal/css_printer/css_printer.go +++ b/internal/css_printer/css_printer.go @@ -444,7 +444,14 @@ func (p *printer) printMediaQuery(query css_ast.MediaQuery, flags mqFlags) { p.printIdent(q.Type, identNormal, 0) if q.AndOrNull.Data != nil { p.print(" and ") - p.printMediaQuery(q.AndOrNull, 0) + // An "or" condition after a media type must be wrapped in parentheses + // to preserve grouping, e.g. "screen and ((a) or (b))" must keep the + // outer parentheses since "screen and (a) or (b)" is invalid CSS. + andFlags := mqFlags(0) + if binary, ok := q.AndOrNull.Data.(*css_ast.MQBinary); ok && binary.Op == css_ast.MQBinaryOpOr { + andFlags = mqNeedsParens + } + p.printMediaQuery(q.AndOrNull, andFlags) } case *css_ast.MQNot: diff --git a/internal/css_printer/css_printer_test.go b/internal/css_printer/css_printer_test.go index f02dbf7dd3..c0403d74e3 100644 --- a/internal/css_printer/css_printer_test.go +++ b/internal/css_printer/css_printer_test.go @@ -370,6 +370,11 @@ func TestAtMedia(t *testing.T) { expectPrintedMinify(t, "@media not ( (a) or (b) ) {div{color:red}}", "@media not ((a)or (b)){div{color:red}}") expectPrintedMinify(t, "@media not ( (a) and (b) ) {div{color:red}}", "@media not ((a)and (b)){div{color:red}}") + // Nested "or" inside a media type must preserve outer parentheses (https://github.com/evanw/esbuild/issues/4395) + expectPrintedMinify(t, "@media only screen and ((min-width: 10px) or (min-height: 10px)) {div{color:red}}", "@media only screen and ((min-width:10px)or (min-height:10px)){div{color:red}}") + expectPrintedMinify(t, "@media screen and ((a) or (b)) {div{color:red}}", "@media screen and ((a)or (b)){div{color:red}}") + expectPrinted(t, "@media only screen and ((min-width: 10px) or (min-height: 10px)) { div { color: red } }", "@media only screen and ((min-width: 10px) or (min-height: 10px)) {\n div {\n color: red;\n }\n}\n") + expectPrintedMinify(t, "@media (width < 2px) {div{color:red}}", "@media(width<2px){div{color:red}}") expectPrintedMinify(t, "@media (1px < width) {div{color:red}}", "@media(1px