diff --git a/internal/css_ast/css_ast.go b/internal/css_ast/css_ast.go index 3ff285fd7f9..50bd0a09998 100644 --- a/internal/css_ast/css_ast.go +++ b/internal/css_ast/css_ast.go @@ -432,8 +432,22 @@ func (r *RAtCharset) Hash() (uint32, bool) { return hash, true } +type ImportConditions struct { + Layers []Token + Supports []Token + Media []Token +} + +func (conditions *ImportConditions) CloneWithImportRecords(importRecordsIn []ast.ImportRecord, importRecordsOut []ast.ImportRecord) (*ImportConditions, []ast.ImportRecord) { + result := ImportConditions{} + result.Layers, importRecordsOut = CloneTokensWithImportRecords(conditions.Layers, importRecordsIn, nil, importRecordsOut) + result.Supports, importRecordsOut = CloneTokensWithImportRecords(conditions.Supports, importRecordsIn, nil, importRecordsOut) + result.Media, importRecordsOut = CloneTokensWithImportRecords(conditions.Media, importRecordsIn, nil, importRecordsOut) + return &result, importRecordsOut +} + type RAtImport struct { - ImportConditions []Token + ImportConditions *ImportConditions ImportRecordIndex uint32 } diff --git a/internal/css_parser/css_parser.go b/internal/css_parser/css_parser.go index 40cdf14cb38..dcf6ef31689 100644 --- a/internal/css_parser/css_parser.go +++ b/internal/css_parser/css_parser.go @@ -1141,6 +1141,7 @@ abortRuleParser: kind = atRuleEmpty p.eat(css_lexer.TWhitespace) if path, r, ok := p.expectURLOrString(); ok { + var conditions css_ast.ImportConditions importConditionsStart := p.index for { if kind := p.current().Kind; kind == css_lexer.TSemicolon || kind == css_lexer.TOpenBrace || @@ -1152,16 +1153,39 @@ abortRuleParser: if p.current().Kind == css_lexer.TOpenBrace { break // Avoid parsing an invalid "@import" rule } - importConditions := p.convertTokens(p.tokens[importConditionsStart:p.index]) + conditions.Media = p.convertTokens(p.tokens[importConditionsStart:p.index]) kind := ast.ImportAt // Insert or remove whitespace before the first token - if len(importConditions) > 0 { + var importConditions *css_ast.ImportConditions + if len(conditions.Media) > 0 { kind = ast.ImportAtConditional - if p.options.minifyWhitespace { - importConditions[0].Whitespace &= ^css_ast.WhitespaceBefore - } else { - importConditions[0].Whitespace |= css_ast.WhitespaceBefore + importConditions = &conditions + + // Handle "layer()" + if t := conditions.Media[0]; (t.Kind == css_lexer.TIdent || t.Kind == css_lexer.TFunction) && t.Text == "layer" { + conditions.Layers = conditions.Media[:1] + conditions.Media = conditions.Media[1:] + } + + // Handle "supports()" + if len(conditions.Media) > 0 { + if t := conditions.Media[0]; t.Kind == css_lexer.TFunction && t.Text == "supports" { + conditions.Supports = conditions.Media[:1] + conditions.Media = conditions.Media[1:] + } + } + + // Remove leading and trailing whitespace + if len(conditions.Layers) > 0 { + conditions.Layers[0].Whitespace &= ^(css_ast.WhitespaceBefore | css_ast.WhitespaceAfter) + } + if len(conditions.Supports) > 0 { + conditions.Supports[0].Whitespace &= ^(css_ast.WhitespaceBefore | css_ast.WhitespaceAfter) + } + if n := len(conditions.Media); n > 0 { + conditions.Media[0].Whitespace &= ^css_ast.WhitespaceBefore + conditions.Media[n-1].Whitespace &= ^css_ast.WhitespaceAfter } } diff --git a/internal/css_printer/css_printer.go b/internal/css_printer/css_printer.go index 6e396dd4fc1..08f805a3e17 100644 --- a/internal/css_printer/css_printer.go +++ b/internal/css_printer/css_printer.go @@ -170,7 +170,29 @@ func (p *printer) printRule(rule css_ast.Rule, indent int32, omitTrailingSemicol } p.printQuoted(record.Path.Text, flags) p.recordImportPathForMetafile(r.ImportRecordIndex) - p.printTokens(r.ImportConditions, printTokensOpts{}) + if conditions := r.ImportConditions; conditions != nil { + space := !p.options.MinifyWhitespace + if len(conditions.Layers) > 0 { + if space { + p.print(" ") + } + p.printTokens(conditions.Layers, printTokensOpts{}) + space = true + } + if len(conditions.Supports) > 0 { + if space { + p.print(" ") + } + p.printTokens(conditions.Supports, printTokensOpts{}) + space = true + } + if len(conditions.Media) > 0 { + if space { + p.print(" ") + } + p.printTokens(conditions.Media, printTokensOpts{}) + } + } p.print(";") case *css_ast.RAtKeyframes: diff --git a/internal/linker/linker.go b/internal/linker/linker.go index 51f9e49c37a..e5f9ef8e71d 100644 --- a/internal/linker/linker.go +++ b/internal/linker/linker.go @@ -194,7 +194,7 @@ type chunkReprCSS struct { type externalImportCSS struct { path logger.Path - conditions []css_ast.Token + conditions *css_ast.ImportConditions conditionImportRecords []ast.ImportRecord } @@ -3337,8 +3337,7 @@ func (c *linkerContext) findImportedCSSFilesInJSOrder(entryPoint uint32) (order // traversal order is B D C A. func (c *linkerContext) findImportedFilesInCSSOrder(entryPoints []uint32) (externalOrder []externalImportCSS, internalOrder []uint32) { type externalImportsCSS struct { - conditions [][]css_ast.Token - unconditional bool + conditions []css_ast.ImportConditions } visited := make(map[uint32]bool) @@ -3376,32 +3375,40 @@ func (c *linkerContext) findImportedFilesInCSSOrder(entryPoints []uint32) (exter // Record external dependencies external := externals[record.Path] - // Check for an unconditional import. An unconditional import - // should always mask all conditional imports that are overridden - // by the unconditional import. - if external.unconditional { - continue + var before css_ast.ImportConditions + if conditions := atImport.ImportConditions; conditions != nil { + before = *conditions } - if len(atImport.ImportConditions) == 0 { - external.unconditional = true - } else { - // Check for a conditional import. A conditional import does not - // mask an earlier unconditional import because re-evaluating a - // CSS file can have observable results. - for _, tokens := range external.conditions { - if css_ast.TokensEqualIgnoringWhitespace(tokens, atImport.ImportConditions) { + // Skip this rule if a later rule masks it + for _, after := range external.conditions { + if css_ast.TokensEqualIgnoringWhitespace(before.Layers, after.Layers) { + if len(after.Supports) == 0 && len(after.Media) == 0 { + // If the later one doesn't have any conditions, only keep + // the later one. The later one will mask the effects of the + // earlier one regardless of whether the earlier one has any + // conditions or not. Only do this if the layers are equal. + continue outer + } + + // If the import conditions are exactly equal, then only keep + // the later one. The earlier one will have no effect. + if css_ast.TokensEqualIgnoringWhitespace(before.Supports, after.Supports) && + css_ast.TokensEqualIgnoringWhitespace(before.Media, after.Media) { continue outer } } - external.conditions = append(external.conditions, atImport.ImportConditions) } - // Clone any import records associated with the condition tokens - conditions, conditionImportRecords := css_ast.CloneTokensWithImportRecords( - atImport.ImportConditions, repr.AST.ImportRecords, nil, nil) - + external.conditions = append(external.conditions, before) externals[record.Path] = external + + var conditions *css_ast.ImportConditions + var conditionImportRecords []ast.ImportRecord + if atImport.ImportConditions != nil { + conditions, conditionImportRecords = atImport.ImportConditions.CloneWithImportRecords(repr.AST.ImportRecords, conditionImportRecords) + } + externalOrder = append(externalOrder, externalImportCSS{ path: record.Path, conditions: conditions, @@ -5712,9 +5719,10 @@ func (c *linkerContext) generateChunkCSS(chunkIndex int, chunkWaitGroup *sync.Wa // Insert all external "@import" rules at the front. In CSS, all "@import" // rules must come first or the browser will just ignore them. for _, external := range chunkRepr.externalImportsInOrder { - var conditions []css_ast.Token - conditions, tree.ImportRecords = css_ast.CloneTokensWithImportRecords( - external.conditions, external.conditionImportRecords, conditions, tree.ImportRecords) + var conditions *css_ast.ImportConditions + if external.conditions != nil { + conditions, tree.ImportRecords = external.conditions.CloneWithImportRecords(external.conditionImportRecords, tree.ImportRecords) + } tree.Rules = append(tree.Rules, css_ast.Rule{Data: &css_ast.RAtImport{ ImportRecordIndex: uint32(len(tree.ImportRecords)), ImportConditions: conditions,