Skip to content

Commit 6b2ee78

Browse files
committed
minify: remove css rules containing empty :is()
1 parent f361deb commit 6b2ee78

3 files changed

Lines changed: 57 additions & 9 deletions

File tree

internal/css_parser/css_parser.go

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -705,8 +705,8 @@ next:
705705
// Mangle non-top-level rules using a back-to-front pass. Top-level rules
706706
// will be mangled by the linker instead for cross-file rule mangling.
707707
if !isTopLevel {
708-
remover := MakeDuplicateRuleMangler(ast.SymbolMap{})
709-
mangledRules = remover.RemoveDuplicateRulesInPlace(p.source.Index, mangledRules, p.importRecords)
708+
remover := MakeDeadRuleMangler(ast.SymbolMap{})
709+
mangledRules = remover.RemoveDeadRulesInPlace(p.source.Index, mangledRules, p.importRecords)
710710
}
711711

712712
return mangledRules
@@ -726,20 +726,20 @@ type callEntry struct {
726726
sourceIndex uint32
727727
}
728728

729-
type DuplicateRuleRemover struct {
729+
type DeadRuleRemover struct {
730730
entries map[uint32]hashEntry
731731
calls []callEntry
732732
check css_ast.CrossFileEqualityCheck
733733
}
734734

735-
func MakeDuplicateRuleMangler(symbols ast.SymbolMap) DuplicateRuleRemover {
736-
return DuplicateRuleRemover{
735+
func MakeDeadRuleMangler(symbols ast.SymbolMap) DeadRuleRemover {
736+
return DeadRuleRemover{
737737
entries: make(map[uint32]hashEntry),
738738
check: css_ast.CrossFileEqualityCheck{Symbols: symbols},
739739
}
740740
}
741741

742-
func (remover *DuplicateRuleRemover) RemoveDuplicateRulesInPlace(sourceIndex uint32, rules []css_ast.Rule, importRecords []ast.ImportRecord) []css_ast.Rule {
742+
func (remover *DeadRuleRemover) RemoveDeadRulesInPlace(sourceIndex uint32, rules []css_ast.Rule, importRecords []ast.ImportRecord) []css_ast.Rule {
743743
// The caller may call this function multiple times, each with a different
744744
// set of import records. Remember each set of import records for equality
745745
// checks later.
@@ -758,6 +758,11 @@ skipRule:
758758
for i := n - 1; i >= 0; i-- {
759759
rule := rules[i]
760760

761+
// Remove rules with selectors that don't apply to anything (e.g. ":is()")
762+
if r, ok := rule.Data.(*css_ast.RSelector); ok && allSelectorsAreDead(r.Selectors) {
763+
continue skipRule
764+
}
765+
761766
// For duplicate rules, omit all but the last copy
762767
if hash, ok := rule.Data.Hash(); ok {
763768
entry := remover.entries[hash]
@@ -795,6 +800,28 @@ skipRule:
795800
return rules[start:]
796801
}
797802

803+
func containsDeadSelectors(selectors []css_ast.CompoundSelector) bool {
804+
for _, sel := range selectors {
805+
for _, ss := range sel.SubclassSelectors {
806+
if pseudo, ok := ss.Data.(*css_ast.SSPseudoClassWithSelectorList); ok && len(pseudo.Selectors) == 0 &&
807+
(pseudo.Kind == css_ast.PseudoClassIs || pseudo.Kind == css_ast.PseudoClassWhere) {
808+
// ":is()" and ":where()" never match anything when empty
809+
return true
810+
}
811+
}
812+
}
813+
return false
814+
}
815+
816+
func allSelectorsAreDead(selectors []css_ast.ComplexSelector) bool {
817+
for _, sel := range selectors {
818+
if !containsDeadSelectors(sel.Selectors) {
819+
return false
820+
}
821+
}
822+
return true
823+
}
824+
798825
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTML/Element
799826
var nonDeprecatedElementsSupportedByIE7 = map[string]bool{
800827
"a": true,

internal/css_parser/css_parser_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2454,6 +2454,27 @@ func TestMangleAlpha(t *testing.T) {
24542454
expectPrintedLowerMangle(t, "a { color: #000000FF }", "a {\n color: #000;\n}\n", "")
24552455
}
24562456

2457+
func TestMangleDeadSelectors(t *testing.T) {
2458+
expectPrinted(t, "a { b { color: red } }", "a {\n b {\n color: red;\n }\n}\n", "")
2459+
expectPrinted(t, "a { :is() { color: red } }", "a {\n :is() {\n color: red;\n }\n}\n", "")
2460+
expectPrinted(t, "a { :where() { color: red } }", "a {\n :where() {\n color: red;\n }\n}\n", "")
2461+
2462+
// Trimming away ":is()" can be relevant for automatically-generated style
2463+
// rules that are the result of a CSS nesting transform. For more info see
2464+
// https://github.com/evanw/esbuild/issues/4265
2465+
expectPrintedMangle(t, "a { color: green; :is() { color: red } }", "a {\n color: green;\n}\n", "")
2466+
expectPrintedMangle(t, "a { color: green; :where() { color: red } }", "a {\n color: green;\n}\n", "")
2467+
expectPrintedMangle(t, "a { color: green; div + :is() { color: red } }", "a {\n color: green;\n}\n", "")
2468+
expectPrintedMangle(t, "a { color: green; div + :where() { color: red } }", "a {\n color: green;\n}\n", "")
2469+
2470+
// Note: Style rules use an unforgiving selector list, so we can't trivially
2471+
// trim away known dead selectors from the list to keep the remaining
2472+
// selectors ("b" in the case below). Other parts of the known dead selector
2473+
// may be invalid, which would then render the whole style rule invalid.
2474+
expectPrintedMangle(t, "a { color: green; b, :foo :is() { color: red } }", "a {\n color: green;\n b,\n :foo :is() {\n color: red;\n }\n}\n", "")
2475+
expectPrintedMangle(t, "a { color: green; b, :foo :where() { color: red } }", "a {\n color: green;\n b,\n :foo :where() {\n color: red;\n }\n}\n", "")
2476+
}
2477+
24572478
func TestMangleDuplicateSelectors(t *testing.T) {
24582479
expectPrinted(t, "a, a { color: red }", "a,\na {\n color: red;\n}\n", "")
24592480
expectPrintedMangle(t, "a, a { color: red }", "a {\n color: red;\n}\n", "")

internal/linker/linker.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6110,9 +6110,9 @@ func (c *linkerContext) generateChunkCSS(chunkIndex int, chunkWaitGroup *sync.Wa
61106110
// in parallel, and must be done from the last rule to the first rule.
61116111
timer.Begin("Prepare CSS ASTs")
61126112
asts := make([]css_ast.AST, len(chunkRepr.importsInChunkInOrder))
6113-
var remover css_parser.DuplicateRuleRemover
6113+
var remover css_parser.DeadRuleRemover
61146114
if c.options.MinifySyntax {
6115-
remover = css_parser.MakeDuplicateRuleMangler(c.graph.Symbols)
6115+
remover = css_parser.MakeDeadRuleMangler(c.graph.Symbols)
61166116
}
61176117
for i := len(chunkRepr.importsInChunkInOrder) - 1; i >= 0; i-- {
61186118
entry := chunkRepr.importsInChunkInOrder[i]
@@ -6208,7 +6208,7 @@ func (c *linkerContext) generateChunkCSS(chunkIndex int, chunkWaitGroup *sync.Wa
62086208

62096209
// Remove top-level duplicate rules across files
62106210
if c.options.MinifySyntax {
6211-
rules = remover.RemoveDuplicateRulesInPlace(entry.sourceIndex, rules, ast.ImportRecords)
6211+
rules = remover.RemoveDeadRulesInPlace(entry.sourceIndex, rules, ast.ImportRecords)
62126212
}
62136213

62146214
ast.Rules = rules

0 commit comments

Comments
 (0)