-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
The order of CSS imports is not respected #465
Comments
Thanks for reporting this. This will be good to fix. I think it's somewhat related to #399 but it's different enough that I'd like to keep this as a separate issue. |
I have the same problem. |
I initially interpreted this to mean "evaluate the CSS file once at the place of the first import in depth-first order" where each file is visited at most once. That's how JavaScript imports work and that's the way esbuild's CSS bundler currently works. But I just realized that this is incorrect. The semantics of CSS @import is more like @include than @import. From the specification:
I'm not sure why it's specified this way since it seems like surprising and unexpected behavior to me. But that's the way CSS actually works. So "bundling CSS imports in the order of their appearance in the module tree" would have to mean "evaluate the CSS file all over again at the place of every import in depth-first order" where the depth-first order can re-visit the same file an unbounded number of times. This can result in a combinatorial explosion of code for deep @import graphs. Here's an example of the difference:
Running this CSS in the browser will result in a font size of 10px but bundling this CSS with esbuild and then running it in the browser will currently result in a font size of 20px. This is because esbuild currently only evaluates each file once at the location of the first @import in depth-first order instead of re-evaluating each file at every @import in depth-first order. I wonder if it would be equivalent to just evaluate the CSS file once at the place of the last import in depth-first order. Instead of having the bundler recursively expand all @import statements, which would be crazy and cause massive code bloat. Placing the CSS last instead of first could potentially break some use cases such as e.g. importing a reset stylesheet from multiple files and then trying to override it below the import (the example above). A file further on in the bundle could re-import the reset stylesheet and erase the overrides due to CSS specificity rules. But that's how CSS works so ¯\(ツ)/¯. A CSS bundler shouldn't not deviate from CSS semantics just because the semantics are unintuitive. I wonder what other bundlers do here. I'm also having trouble figuring out what ordering even means for dynamic JavaScript imports that then import CSS code (see #608 (comment)), but that's another topic. Edit: Here is a quick survey of some different CSS environments for the example above:
It's also interesting to compare how these different environments handle a cycle in the import graph:
|
I have been very curious why the import mechanism in CSS is so inconvenient to use. I finally found the rationale for why CSS Some of the reasons for this behavior from thread:
Still seems like a pretty short-sighted decision to me. Other people in the thread mention confusion about their import rules being silently ignored. And this behavior means esbuild has to potentially generate lots of extra imports when bundling to preserve CSS import order so this restriction introduces more network overhead. Oh well. The ship has sailed. |
Note: I'm just commenting about this here to document my thinking. So the main problem is with external imports. Consider a case where you have @imports to external CSS (outside of the bundle) that come after @imports to internal CSS (inside the bundle): /* entry.css */
@import "./internal.css";
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
.someDiv { ... } /* internal.css */
.someDiv2 { ... } CSS says that @import has to come first, so "hoisting" external imports to the top like this would be invalid: /* This is different than the original CSS evaluation order */
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
.someDiv2 { ... }
.someDiv { ... } That would cause it to be evaluated after the external CSS instead of before. One solution would be to generate another chunk like this: /* entry.css */
@import "./chunk.HASH.css";
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
.someDiv { ... } /* chunk.HASH.css */
.someDiv2 { ... } That's not great obviously because then the code isn't bundled anymore, at least not into a single file. This is where the extra network overhead that I was talking about earlier comes from. I previously thought that this is the best you can do. But I just realized that there's another hack you can do instead: /* entry.css */
@import "data:text/css,\
.someDiv2 { ... }\
";
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
.someDiv { ... } This feels gross but it does fit everything into a single file and it seems to work, at least in modern browsers. I'm not sure what browser support looks like but it might actually be ok to have this be the primary bundling strategy. This is basically a workaround for not being able to stick @import in the middle of a file. |
🥳 |
Not sure whether this is related to #399 or a separate issue.
Test case
Consider the following project:
Actual behavior
If you bundle the project with ESBuild:
the resulting
build/index.css
file will look as follows:The problem here is that ESBuild changes the order of CSS modules. This changes the specificity of rules – and, ultimately, changes how the page would look. (The text would be green instead of blue.)
Expected behavior
The expected behavior is for ESBuild to bundle CSS imports in the order of their appearance in the module tree (with depth-first traversal). That’s how webpack does that (AFAIK).
You can test the webpack behavior with the following config:
package.json
:webpack.config.js
:Building the project with the above config (
npx webpack
) producesbuild/main.css
that looks as follows:The text was updated successfully, but these errors were encountered: