Skip to content

Commit

Permalink
fix #953, fix #3137: advanced css @import syntax
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Aug 7, 2023
1 parent 6892b1b commit 4bb897f
Show file tree
Hide file tree
Showing 9 changed files with 524 additions and 68 deletions.
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

## Unreleased

* Support advanced CSS `@import` rules ([#953](https://github.com/evanw/esbuild/issues/953), [#3137](https://github.com/evanw/esbuild/issues/3137))

CSS `@import` statements have been extended to allow additional trailing tokens after the import path. These tokens sort of make the imported file behave as if it were wrapped in a `@layer`, `@supports`, and/or `@media` rule. Here are some examples:

```css
@import url(foo.css);
@import url(foo.css) layer;
@import url(foo.css) layer(bar);
@import url(foo.css) layer(bar) supports(display: flex);
@import url(foo.css) layer(bar) supports(display: flex) print;
@import url(foo.css) layer(bar) print;
@import url(foo.css) supports(display: flex);
@import url(foo.css) supports(display: flex) print;
@import url(foo.css) print;
```

You can read more about this advanced syntax [here](https://developer.mozilla.org/en-US/docs/Web/CSS/@import). With this release, esbuild will now bundle `@import` rules with these trailing tokens and will wrap the imported files in the corresponding rules. Note that this now means a given imported file can potentially appear in multiple places in the bundle. However, esbuild will still only load it once (e.g. on-load plugins will only run once per file, not once per import).

## 0.18.19

* Implement `composes` from CSS modules ([#20](https://github.com/evanw/esbuild/issues/20))
Expand Down
9 changes: 3 additions & 6 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ const (
// A CSS "@import" rule
ImportAt

// A CSS "@import" rule with import conditions
ImportAtConditional

// A CSS "composes" declaration
ImportComposesFrom

Expand All @@ -52,7 +49,7 @@ func (kind ImportKind) StringForMetafile() string {
return "dynamic-import"
case ImportRequireResolve:
return "require-resolve"
case ImportAt, ImportAtConditional:
case ImportAt:
return "import-rule"
case ImportComposesFrom:
return "composes-from"
Expand All @@ -67,15 +64,15 @@ func (kind ImportKind) StringForMetafile() string {

func (kind ImportKind) IsFromCSS() bool {
switch kind {
case ImportAt, ImportAtConditional, ImportComposesFrom, ImportURL:
case ImportAt, ImportComposesFrom, ImportURL:
return true
}
return false
}

func (kind ImportKind) MustResolveToCSS() bool {
switch kind {
case ImportAt, ImportAtConditional, ImportComposesFrom:
case ImportAt, ImportComposesFrom:
return true
}
return false
Expand Down
5 changes: 1 addition & 4 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2019,17 +2019,14 @@ func (s *scanner) processScannedFiles(entryPointMeta []graph.EntryPoint) []scann
otherFile.inputFile.Source.PrettyPath, config.LoaderToString[otherFile.inputFile.Loader])}})
}

case ast.ImportAt, ast.ImportAtConditional:
case ast.ImportAt:
// Using a JavaScript file with CSS "@import" is not allowed
if _, ok := otherFile.inputFile.Repr.(*graph.JSRepr); ok && otherFile.inputFile.Loader != config.LoaderEmpty {
s.log.AddErrorWithNotes(&tracker, record.Range,
fmt.Sprintf("Cannot import %q into a CSS file", otherFile.inputFile.Source.PrettyPath),
[]logger.MsgData{{Text: fmt.Sprintf(
"An \"@import\" rule can only be used to import another CSS file and %q is not a CSS file (it was loaded with the %q loader).",
otherFile.inputFile.Source.PrettyPath, config.LoaderToString[otherFile.inputFile.Loader])}})
} else if record.Kind == ast.ImportAtConditional {
s.log.AddError(&tracker, record.Range,
"Bundling with conditional \"@import\" rules is not currently supported")
}

case ast.ImportURL:
Expand Down
77 changes: 73 additions & 4 deletions internal/bundler_tests/bundler_css_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1347,16 +1347,85 @@ func TestCSSAtImportConditionsBundleExternalConditionWithURL(t *testing.T) {
func TestCSSAtImportConditionsBundle(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `@import "./print.css" print;`,
"/print.css": `body { color: red }`,
"/entry.css": `
@import url(http://example.com/foo.css);
@import url(http://example.com/foo.css) layer;
@import url(http://example.com/foo.css) layer(layer-name);
@import url(http://example.com/foo.css) layer(layer-name) supports(supports-condition);
@import url(http://example.com/foo.css) layer(layer-name) supports(supports-condition) list-of-media-queries;
@import url(http://example.com/foo.css) layer(layer-name) list-of-media-queries;
@import url(http://example.com/foo.css) supports(supports-condition);
@import url(http://example.com/foo.css) supports(supports-condition) list-of-media-queries;
@import url(http://example.com/foo.css) list-of-media-queries;
@import url(foo.css);
@import url(foo.css) layer;
@import url(foo.css) layer(layer-name);
@import url(foo.css) layer(layer-name) supports(supports-condition);
@import url(foo.css) layer(layer-name) supports(supports-condition) list-of-media-queries;
@import url(foo.css) layer(layer-name) list-of-media-queries;
@import url(foo.css) supports(supports-condition);
@import url(foo.css) supports(supports-condition) list-of-media-queries;
@import url(foo.css) list-of-media-queries;
@import url(empty-1.css) layer(empty-1);
@import url(empty-2.css) supports(empty: 2);
@import url(empty-3.css) (empty: 3);
@import "nested-layer.css" layer(outer);
@import "nested-layer.css" supports(outer: true);
@import "nested-layer.css" (outer: true);
@import "nested-supports.css" layer(outer);
@import "nested-supports.css" supports(outer: true);
@import "nested-supports.css" (outer: true);
@import "nested-media.css" layer(outer);
@import "nested-media.css" supports(outer: true);
@import "nested-media.css" (outer: true);
`,

"/foo.css": `body { color: red }`,

"/empty-1.css": ``,
"/empty-2.css": ``,
"/empty-3.css": ``,

"/nested-layer.css": `@import "foo.css" layer(inner);`,
"/nested-supports.css": `@import "foo.css" supports(inner: true);`,
"/nested-media.css": `@import "foo.css" (inner: true);`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
},
expectedScanLog: `entry.css: ERROR: Bundling with conditional "@import" rules is not currently supported
`,
})
}

func TestCSSAtImportConditionsWithImportRecordsBundle(t *testing.T) {
// This tests that esbuild correctly clones the import records for all import
// condition tokens. If they aren't cloned correctly, then something will
// likely crash with an out-of-bounds error.
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.css": `
@import url(foo.css) supports(background: url(a.png));
@import url(foo.css) supports(background: url(b.png)) list-of-media-queries;
@import url(foo.css) layer(layer-name) supports(background: url(a.png));
@import url(foo.css) layer(layer-name) supports(background: url(b.png)) list-of-media-queries;
`,
"/foo.css": `body { color: red }`,
"/a.png": `A`,
"/b.png": `B`,
},
entryPaths: []string{"/entry.css"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputFile: "/out.css",
ExtensionToLoader: map[string]config.Loader{
".css": config.LoaderCSS,
".png": config.LoaderBase64,
},
},
})
}

Expand Down
Loading

0 comments on commit 4bb897f

Please sign in to comment.