v0.12.0
This release contains backwards-incompatible changes. Since esbuild is before version 1.0.0, these changes have been released as a new minor version to reflect this (as recommended by npm). You should either be pinning the exact version of esbuild
in your package.json
file or be using a version range syntax that only accepts patch upgrades such as ~0.11.0
. See the documentation about semver for more information.
The breaking changes in this release relate to CSS import order and also build scenarios where both the inject
and define
API options are used (see below for details). These breaking changes are as follows:
-
Fix bundled CSS import order (#465)
JS and CSS use different import ordering algorithms. In JS, importing a file that has already been imported is a no-op but in CSS, importing a file that has already been imported re-imports the file. A simple way to imagine this is to view each
@import
rule in CSS as being replaced by the contents of that file similar to#include
in C/C++. However, this is incorrect in the case of@import
cycles because it would cause infinite expansion. A more accurate way to imagine this is that in CSS, a file is evaluated at the last@import
location while in JS, a file is evaluated at the firstimport
location.Previously esbuild followed JS import order rules for CSS but now esbuild will follow CSS import order rules. This is a breaking change because it means your CSS may behave differently when bundled. Note that CSS import order rules are somewhat unintuitive because evaluation order matters. In CSS, using
@import
multiple times can end up unintentionally erasing overriding styles. For example, consider the following files:/* entry.css */ @import "./color.css"; @import "./background.css";
/* color.css */ @import "./reset.css"; body { color: white; }
/* background.css */ @import "./reset.css"; body { background: black; }
/* reset.css */ body { background: white; color: black; }
Because of how CSS import order works,
entry.css
will now be bundled like this:/* color.css */ body { color: white; } /* reset.css */ body { background: white; color: black; } /* background.css */ body { background: black; }
This means the body will unintuitively be all black! The file
reset.css
is evaluated at the location of the last@import
instead of the first@import
. The fix for this case is to remove the nested imports ofreset.css
and to importreset.css
exactly once at the top ofentry.css
.Note that while the evaluation order of external CSS imports is preserved with respect to other external CSS imports, the evaluation order of external CSS imports is not preserved with respect to other internal CSS imports. All external CSS imports are "hoisted" to the top of the bundle. The alternative would be to generate many smaller chunks which is usually undesirable. So in this case esbuild's CSS bundling behavior will not match the browser.
-
Fix bundled CSS when using JS code splitting (#608)
Previously esbuild generated incorrect CSS output when JS code splitting was enabled and the JS code being bundled imported CSS files. CSS code that was reachable via multiple JS entry points was split off into a shared CSS chunk, but that chunk was not actually imported anywhere so the shared CSS was missing. This happened because both CSS and JS code splitting were experimental features that are still in progress and weren't tested together.
Now esbuild's CSS output should contain all reachable CSS code when JS code splitting is enabled. Note that this does not mean code splitting works for CSS files. Each CSS output file simply contains the transitive set of all CSS reachable from the JS entry point including through dynamic
import()
andrequire()
expressions. Specifically, the bundler constructs a virtual CSS file for each JS entry point consisting only of@import
rules for each CSS file imported into a JS file. These@import
rules are constructed in JS source order, but then the bundler uses CSS import order from that point forward to bundle this virtual CSS file into the final CSS output file.This model makes the most sense when CSS files are imported into JS files via JS
import
statements. Importing CSS viaimport()
andrequire()
(either directly or transitively through multiple intermediate JS files) should still "work" in the sense that all reachable CSS should be included in the output, but in this case esbuild will pick an arbitrary (but consistent) import order. The import order may not match the order that the JS files are evaluated in because JS evaluation order of dynamic imports is only determined at run-time while CSS bundling happens at compile-time.It's possible to implement code splitting for CSS such that CSS code used between multiple entry points is shared. However, CSS lacks a mechanism for "lazily" importing code (i.e. disconnecting the import location with the evaluation location) so CSS code splitting could potentially need to generate a huge number of very small chunks to preserve import order. It's unclear if this would end up being a net win or not as far as browser download time. So sharing-based code splitting is currently not supported for CSS.
It's theoretically possible to implement code splitting for CSS such that CSS from a dynamically-imported JS file (e.g. via
import()
) is placed into a separate chunk. However, due to how@import
order works this would in theory end up re-evaluating all shared dependencies which could overwrite overloaded styles and unintentionally change the way the page is rendered. For example, constructing a single-page app architecture such that each page is JS-driven and can transition to other JS-driven pages viaimport()
could end up with pages that look different depending on what order you visit them in. This is clearly undesirable. The simple way to address this is to just not support dynamic-import code splitting for CSS either. -
Change "define" to have higher priority than "inject" (#660)
The "define" and "inject" features are both ways of replacing certain expressions in your source code with other things expressions. Previously esbuild's behavior ran "inject" before "define", which could lead to some undesirable behavior. For example (from the
react
npm package):if (process.env.NODE_ENV === 'production') { module.exports = require('./cjs/react.production.min.js'); } else { module.exports = require('./cjs/react.development.js'); }
If you use "define" to replace
process.env.NODE_ENV
with"production"
and "inject" to replaceprocess
with a shim that emulates node's process API, thenprocess
was previously replaced first and thenprocess.env.NODE_ENV
wasn't matched becauseprocess
referred to the injected shim. This wasn't ideal because it means esbuild didn't detect the branch condition as a constant (since it doesn't know how the shim behaves at run-time) and bundled both the development and production versions of the package.With this release, esbuild will now run "define" before "inject". In the above example this means that
process.env.NODE_ENV
will now be replaced with"production"
, the injected shim will not be included, and only the production version of the package will be bundled. This feature was contributed by @rtsao.
In addition to the breaking changes above, the following features are also included in this release:
-
Add support for the
NO_COLOR
environment variableThe CLI will now omit color if the
NO_COLOR
environment variable is present, which is an existing convention that is followed by some other software. See https://no-color.org/ for more information.