I want esbuild to demonstrate that it's possible for a normal web development workflow to have high-performance tools. I consider bundling CSS modules a part of my initial MVP feature set for esbuild since some form of CSS modularity is essential for a large-scale web app and I find CSS modules an elegant solution. This issue tracks the CSS module bundling part of my MVP.
At the bare minimum, importing a CSS file should ensure it's included in the bundle. I plan to also implement CSS modules, which makes CSS selector names local to the file and allows JavaScript to import the selector names as strings.
There are interesting extensions to think about once I get everything working. Tree shaking of unused CSS is an optimization that seems worth exploring, for example. That should be possible with CSS modules since unused imports correspond to unused CSS.
Would this include font files, or is that a separate task?
Binary resources that don't need to be parsed such as PNG, JPEG, and font files are all the same to a bundler. Fonts don't need to be special cased as far as I'm aware. I think the existing loaders such as dataurl
and file
should cover fonts.
I'm starting to work on CSS support. I just landed a basic parser and AST, and will be working on integrating it into the build system next.
Note that I'm not planning to support the full CSS ecosystem with this feature. Today's CSS is a very diverse ecosystem with many non-standard language variants. This is possible in part due to the fault-tolerant nature of CSS. I'm imagining this feature as mainly a "CSS linker" that is responsible for the critical parts of joining CSS files together and removing unused CSS code. The CSS parser will be expecting real CSS for use with browsers. Non-standard extensions may still be parsed without syntax errors but may be passed through unmodified (e.g. not properly minified or renamed) since they aren't fully understood by the parser. It should be possible to use syntax extensions (e.g. SASS) with a plugin that transforms the source code before esbuild reads it.
@evanw that's great to hear! There are different kinds of CSS modules out there, and there is also ongoing for a web standard for CSS modules (see https://github.com/whatwg/html/pull/4898 and https://github.com/tc39/proposal-import-assertions). WIll esbuild allow specifying which variant to use?
@evanw will the CSS feature inline the minified CSS in the javascript bundle or emit it as a separate file? While inlining is a good fit for client rendered applications, it is not ideal for server or static rendered applications. Ideally, esbuild would emit a number of CSS chunks, and the metafile would detail which CSS chunks correspond to each javascript entrypoint. A server or build tool would then be able to add references to the CSS for each page using <link>
tags in the HTML it generates.
Import assertions seem unrelated to CSS to me. They just look like a way to tell the runtime what the file type is. Bundlers already have a way of doing this: they just use the file extension without needing an inline annotation, so this information is not necessary for bundlers.
This seems like an API to inject normal CSS inside a shadow DOM element? It looks like it would just be normal CSS as far as the bundler is concerned.
The CSS module implementation I'm thinking of adopting is the original one: https://github.com/css-modules/css-modules. Specifically, the use of local-by-default names, the :local
and :global
selectors, the composes
declarations, and being able to import local names as strings in JavaScript.
will the CSS feature inline the minified CSS in the javascript bundle or emit it as a separate file?
It will be in a separate file. I believe this is the current expected behavior. It's how Parcel works, for example. I was not considering inlining it in the JavaScript code. I am also planning on having code splitting work the same way across JavaScript and CSS files.
I listed import assertions just to indicate that there will be a standard way to import non-JS files.
The idea for the standard CSS modules is that they will return an instance of CSSStyleSheet
(https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet and https://developers.google.com/web/updates/2019/02/constructable-stylesheets). So a "polyfill" would instantiate it with the css text. Shadow roots have a nice API for using those stylesheets, but they're not necessarily coupled to it. Hope fully it will be adopted more broadly.
My main concern is about what's enabled by default in esbuild, so far most (all?) things require an explicit opt-in which I think is important.
The idea for the standard CSS modules is that they will return an instance of
CSSStyleSheet
(https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet and https://developers.google.com/web/updates/2019/02/constructable-stylesheets). So a "polyfill" would instantiate it with the css text. Shadow roots have a nice API for using those stylesheets, but they're not necessarily coupled to it. Hope fully it will be adopted more broadly.
Interesting, thanks for the clarification. FWIW I think this should be trivial to do with a plugin. That will let you experiment with this proposal.
My main concern is about what's enabled by default in esbuild, so far most (all?) things require an explicit opt-in which I think is important.
My current plan:
By default, esbuild will use the css
loader for .css
files which will only interpret @import
statements and use those for bundling CSS together. This should also naturally work with code splitting without any additional configuration. When a CSS file is used as an entry point, the resulting output file will also be a CSS file without any JavaScript file (i.e. CSS will have first-class support).
When a JavaScript file does import "./file.css"
that will cause the generation of a parallel set of CSS files alongside the output files for the JavaScript entry points. So if you run esbuild on app.ts
and that imports a JavaScript file that imports a CSS file, the output folder will contain app.js
and app.css
. When a JavaScript file does import * as styles from "./file.css"
the styles
object will be empty (no properties).
CSS modules are opt-in by using the css-module
loader. I may have a default extension mapping built-in for .module.css
to override the behavior of .css
, since that's a common convention in the wild. Only then will import * as styles from "./file.css"
have any properties. This loader will still have the same output file generation behavior as the normal CSS loader. All imported CSS files will be bundled together into a single CSS output file (or perhaps more than one if code splitting is active and there are multiple entry points).
Thanks, I think that default makes sense 馃憤
That behavior sounds ideal. Can't wait for it!
The newly-released version 0.7.7 now has experimental CSS support. From the release notes:
This release introduces the new
css
loader, enabled by default for.css
files. It has the following features:
You can now use esbuild to process CSS files by passing a CSS file as an entry point. This means CSS is a new first-class file type and you can use it without involving any JavaScript code at all.
When bundling is enabled, esbuild will bundle multiple CSS files together if they are referenced using the
@import "./file.css";
syntax. CSS files can be excluded from the bundle by marking them as external similar to JavaScript files.There is basic support for pretty-printing CSS, and for whitespace removal when the
--minify
flag is present. There isn't any support for CSS syntax compression yet. Note that pretty-printing and whitespace removal both rely on the CSS syntax being recognized. Currently esbuild only recognizes certain CSS syntax and passes through unrecognized syntax unchanged.Some things to keep in mind:
CSS support is a significant undertaking and this is the very first release. There are almost certainly going to be issues. This is an experimental release to land the code and get feedback.
There is no support for CSS modules yet. Right now all class names are in the global namespace. Importing a CSS file into a JavaScript file will not result in any import names.
There is currently no support for code splitting of CSS. I haven't tested multiple entry-point scenarios yet and code splitting will require additional changes to the AST format.
There's still a long way to go but this feels like a good point to publish what's there so far. It should already be useful for a limited set of use cases, and then I will expand use cases over time.
This feature will be especially useful with the addition of the plugin API (see issue #111) because then esbuild's bundler can be a "CSS linker" that runs on the output of whatever CSS post-processor you're using. The plugin API hasn't been released yet because I wanted to get basic CSS support in first so that there are at least two different core file types for the API to abstract over.
FYI for people following this issue: #415 was recently implemented making it possible to bundle url(...)
references to images in CSS.
Hey there, good work on this.
Just wanted to share my experience of trying this out. I was hoping that import styles from "./my-css-file.css"
would yield a CSSStyleSheet
that can be used in conjunction with Constructable Stylesheets, as @LarsDenBakker was also pointing out in this comment. As it stands right now, I'm actually not seeing how I might be able to retrieve the StyleSheet or a string representing it such that I can work with it (without implementing it in JS/TS).
I'm aware that there is a gazillion ways of handling styles on the web. I'm also aware that the expectations for how this might work varies greatly from web developer to web developer. I'm also aware that many tools do the _module-returning-class-names_ thing, probably the vast majority. But I would argue strongly in favor of .css
files becoming modules with a default export pointing to a CSSStyleSheet
. For the following reasons:
This is in line with current standardization efforts as seen with Import Assertions which is the foundation behind the JSON Modules proposal (which will most likely also be foundation behind CSS Modules).
This allows for passing styles to a root (such as the document element or any Shadow Root) via adoptedStyleSheets
.
This simplifies the implementation. In time, developers can leverage the plugin infrastructure to "override" the default behavior for different behavior, for example old-fashioned CSS Modules that returns an object of class names, etc.
Web developers who work with Shadow DOM are relying on being able to append stylesheets to a root at any point in the DOM tree, and won't benefit from, say, the imported styles being appended to the document root.
@wessberg Can you confirm if the plugin API lets you write a plugin to enable the CSSStyleSheet
use case? I'm unfamiliar with the proposal but I imagine writing a plugin for this should be pretty simple if you just need to construct an object and return it.
The form of CSS modules where class names are local to the file and are exported to JavaScript seems better for building scalable web applications to me. That enables tree shaking and code splitting for CSS, which other solutions don't support AFAIK. Tree shaking of CSS isn't really possible to build as an esbuild plugin because it requires deep integration with the JavaScript layer. So I am still planning to explore this in esbuild itself.
I will test it out and get back to you :-)
And again, while I realize some other bundlers are doing something similar to what you're planning for with CSS support in esbuild
, I'm just seeing a pretty massive gap between what is being worked on in the standards bodies and what is being implemented or have already been implemented across some bundlers. This might lead to confusion when the native runtime does things rapidly different to bundlers.
Importantly, it leaves out everyone working with Shadow DOM where styles are applied at roots across the DOM tree rather than globally at the document root, and where styles are _already_ local to the tree from the Shadow root they are being applied to. These users will either need to declare the styles in JS/TS and either inline them in a <style>
tag appended to the root or manually generate CSSStyleSheets from them or write a custom plugin as suggested, and add it to adoptedStyleSheets for that root.
I think that the more "standards-compliant" (I put this in quotes, because standardization efforts are still ongoing here) approach should be default, and the more application-specific behavior should come from plugins.
But I of course respect your decision. I come from Rollup, which is pretty founded in being as close to native ES module semantics as practically possible, but there are obvious advantages to esbuild
(ease of use, performance), and I want this tool to succeed for everyone, including those that use Shadow DOM :-)
Most helpful comment
The newly-released version 0.7.7 now has experimental CSS support. From the release notes:
There's still a long way to go but this feels like a good point to publish what's there so far. It should already be useful for a limited set of use cases, and then I will expand use cases over time.
This feature will be especially useful with the addition of the plugin API (see issue #111) because then esbuild's bundler can be a "CSS linker" that runs on the output of whatever CSS post-processor you're using. The plugin API hasn't been released yet because I wanted to get basic CSS support in first so that there are at least two different core file types for the API to abstract over.