Compare Versions - esbuild
npm / esbuild / Compare Versions
-
Fix a regression with CSS media queries (#4395, #4405, #4406)
Version 0.25.11 of esbuild introduced support for parsing media queries. This unintentionally introduced a regression with printing media queries that use the
<media-type> and <media-condition-without-or>grammar. Specifically, esbuild was failing to wrap anorclause with parentheses when inside<media-condition-without-or>. This release fixes the regression.Here is an example:
/* Original code */ @media only screen and ((min-width: 10px) or (min-height: 10px)) { a { color: red } } /* Old output (incorrect) */ @media only screen and (min-width: 10px) or (min-height: 10px) { a { color: red; } } /* New output (correct) */ @media only screen and ((min-width: 10px) or (min-height: 10px)) { a { color: red; } } -
Fix an edge case with the
injectfeature (#4407)This release fixes an edge case where esbuild's
injectfeature could not be used with arbitrary module namespace names exported using anexport {} fromstatement with bundling disabled and a target environment where arbitrary module namespace names is unsupported.With the fix, the following
injectfile:import jquery from 'jquery'; export { jquery as 'window.jQuery' };Can now always be rewritten as this without esbuild sometimes incorrectly generating an error:
export { default as 'window.jQuery' } from 'jquery'; -
Attempt to improve API handling of huge metafiles (#4329, #4415)
This release contains a few changes that attempt to improve the behavior of esbuild's JavaScript API with huge metafiles (esbuild's name for the build metadata, formatted as a JSON object). The JavaScript API is designed to return the metafile JSON as a JavaScript object in memory, which makes it easy to access from within a JavaScript-based plugin. Multiple people have encountered issues where this API breaks down with a pathologically-large metafile.
The primary issue is that V8 has an implementation-specific maximum string length, so using the
JSON.parseAPI with large enough strings is impossible. This release will now attempt to use a fallback JavaScript-based JSON parser that operates directly on the UTF8-encoded JSON bytes instead of usingJSON.parsewhen the JSON metafile is too big to fit in a JavaScript string. The new fallback path has not yet been heavily-tested. The metafile will also now be generated with whitespace removed if the bundle is significantly large, which will reduce the size of the metafile JSON slightly.However, hitting this case is potentially a sign that something else is wrong. Ideally you wouldn't be building something so enormous that the build metadata can't even fit inside a JavaScript string. You may want to consider optimizing your project, or breaking up your project into multiple parts that are built independently. Another option could potentially be to use esbuild's command-line API instead of its JavaScript API, which is more efficient (although of course then you can't use JavaScript plugins, so it may not be an option).
-
Preserve URL fragments in data URLs (#4370)
Consider the following HTML, CSS, and SVG:
-
index.html:<!DOCTYPE html> <html> <head><link rel="stylesheet" href="icons.css"></head> <body><div class="triangle"></div></body> </html> -
icons.css:.triangle { width: 10px; height: 10px; background: currentColor; clip-path: url(./triangle.svg#x); } -
triangle.svg:<svg xmlns="http://www.w3.org/2000/svg"> <defs> <clipPath id="x"> <path d="M0 0H10V10Z"/> </clipPath> </defs> </svg>
The CSS uses a URL fragment (the
#x) to reference theclipPathelement in the SVG file. Previously esbuild's CSS bundler didn't preserve the URL fragment when bundling the SVG using thedataurlloader, which broke the bundled CSS. With this release, esbuild will now preserve the URL fragment in the bundled CSS:/* icons.css */ .triangle { width: 10px; height: 10px; background: currentColor; clip-path: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><defs><clipPath id="x"><path d="M0 0H10V10Z"/></clipPath></defs></svg>#x'); } -
-
Parse and print CSS
@scoperules (#4322)This release includes dedicated support for parsing
@scoperules in CSS. These rules include optional "start" and "end" selector lists. One important consequence of this is that the local/global status of names in selector lists is now respected, which improves the correctness of esbuild's support for CSS modules. Minification of selectors inside@scoperules has also improved slightly.Here's an example:
/* Original code */ @scope (:global(.foo)) to (:local(.bar)) { .bar { color: red; } } /* Old output (with --loader=local-css --minify) */ @scope (:global(.foo)) to (:local(.bar)){.o{color:red}} /* New output (with --loader=local-css --minify) */ @scope(.foo)to (.o){.o{color:red}} -
Fix a minification bug with lowering of
for await(#4378, #4385)This release fixes a bug where the minifier would incorrectly strip the variable in the automatically-generated
catchclause of loweredfor awaitloops. The code that generated the loop previously failed to mark the internal variable references as used. -
Update the Go compiler from v1.25.5 to v1.25.7 (#4383, #4388)
This PR was contributed by @MikeWillCook.
-
Allow import path specifiers starting with
#/(#4361)Previously the specification for
package.jsondisallowed import path specifiers starting with#/, but this restriction has recently been relaxed and support for it is being added across the JavaScript ecosystem. One use case is using it for a wildcard pattern such as mapping#/*to./src/*(previously you had to use another character such as#_*instead, which was more confusing). There is some more context in nodejs/node#49182.This change was contributed by @hybrist.
-
Automatically add the
-webkit-maskprefix (#4357, #4358)This release automatically adds the
-webkit-vendor prefix for themaskCSS shorthand property:/* Original code */ main { mask: url(x.png) center/5rem no-repeat } /* Old output (with --target=chrome110) */ main { mask: url(x.png) center/5rem no-repeat; } /* New output (with --target=chrome110) */ main { -webkit-mask: url(x.png) center/5rem no-repeat; mask: url(x.png) center/5rem no-repeat; }This change was contributed by @BPJEnnova.
-
Additional minification of
switchstatements (#4176, #4359)This release contains additional minification patterns for reducing
switchstatements. Here is an example:// Original code switch (x) { case 0: foo() break case 1: default: bar() } // Old output (with --minify) switch(x){case 0:foo();break;case 1:default:bar()} // New output (with --minify) x===0?foo():bar(); -
Forbid
usingdeclarations insideswitchclauses (#4323)This is a rare change to remove something that was previously possible. The Explicit Resource Management proposal introduced
usingdeclarations. These were previously allowed insidecaseanddefaultclauses inswitchstatements. This had well-defined semantics and was already widely implemented (by V8, SpiderMonkey, TypeScript, esbuild, and others). However, it was considered to be too confusing because of how scope works in switch statements, so it has been removed from the specification. This edge case will now be a syntax error. See tc39/proposal-explicit-resource-management#215 and rbuckton/ecma262#14 for details.Here is an example of code that is no longer allowed:
switch (mode) { case 'read': using readLock = db.read() return readAll(readLock) case 'write': using writeLock = db.write() return writeAll(writeLock) }That code will now have to be modified to look like this instead (note the additional
{and}block statements around each case body):switch (mode) { case 'read': { using readLock = db.read() return readAll(readLock) } case 'write': { using writeLock = db.write() return writeAll(writeLock) } }This is not being released in one of esbuild's breaking change releases since this feature hasn't been finalized yet, and esbuild always tracks the current state of the specification (so esbuild's previous behavior was arguably incorrect).
-
Fix bundler bug with
varnested insideif(#4348)This release fixes a bug with the bundler that happens when importing an ES module using
require(which causes it to be wrapped) and there's a top-levelvarinside anifstatement without being wrapped in a{ ... }block (and a few other conditions). The bundling transform needed to hoist thesevardeclarations outside of the lazy ES module wrapper for correctness. See the issue for details. -
Fix minifier bug with
forinsidetryinside label (#4351)This fixes an old regression from version v0.21.4. Some code was introduced to move the label inside the
trystatement to address a problem with transforming labeledfor awaitloops to avoid theawait(the transformation involves converting thefor awaitloop into aforloop and wrapping it in atrystatement). However, it introduces problems for cross-compiled JVM code that uses all three of these features heavily. This release restricts this transform to only apply toforloops that esbuild itself generates internally as part of thefor awaittransform. Here is an example of some affected code:// Original code d: { e: { try { while (1) { break d } } catch { break e; } } } // Old output (with --minify) a:try{e:for(;;)break a}catch{break e} // New output (with --minify) a:e:try{for(;;)break a}catch{break e} -
Inline IIFEs containing a single expression (#4354)
Previously inlining of IIFEs (immediately-invoked function expressions) only worked if the body contained a single
returnstatement. Now it should also work if the body contains a single expression statement instead:// Original code const foo = () => { const cb = () => { console.log(x()) } return cb() } // Old output (with --minify) const foo=()=>(()=>{console.log(x())})(); // New output (with --minify) const foo=()=>{console.log(x())}; -
The minifier now strips empty
finallyclauses (#4353)This improvement means that
finallyclauses containing dead code can potentially cause the associatedtrystatement to be removed from the output entirely in minified builds:// Original code function foo(callback) { if (DEBUG) stack.push(callback.name); try { callback(); } finally { if (DEBUG) stack.pop(); } } // Old output (with --minify --define:DEBUG=false) function foo(a){try{a()}finally{}} // New output (with --minify --define:DEBUG=false) function foo(a){a()} -
Allow tree-shaking of the
SymbolconstructorWith this release, calling
Symbolis now considered to be side-effect free when the argument is known to be a primitive value. This means esbuild can now tree-shake module-level symbol variables:// Original code const a = Symbol('foo') const b = Symbol(bar) // Old output (with --tree-shaking=true) const a = Symbol("foo"); const b = Symbol(bar); // New output (with --tree-shaking=true) const b = Symbol(bar);
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.26.0 or ~0.26.0. See npm's documentation about semver for more information.
-
Use
Uint8Array.fromBase64if available (#4286)With this release, esbuild's
binaryloader will now use the newUint8Array.fromBase64function unless it's unavailable in the configured target environment. If it's unavailable, esbuild's previous code for this will be used as a fallback. Note that this means you may now need to specifytargetwhen using this feature with Node (for example--target=node22) unless you're using Node v25+. -
Update the Go compiler from v1.23.12 to v1.25.4 (#4208, #4311)
This raises the operating system requirements for running esbuild:
- Linux: now requires a kernel version of 3.2 or later
- macOS: now requires macOS 12 (Monterey) or later
-
Enable trusted publishing (#4281)
GitHub and npm are recommending that maintainers for packages such as esbuild switch to trusted publishing. With this release, a VM on GitHub will now build and publish all of esbuild's packages to npm instead of me. In theory.
Unfortunately there isn't really a way to test that this works other than to do it live. So this release is that live test. Hopefully this release is uneventful and is exactly the same as the previous one (well, except for the green provenance attestation checkmark on npm that happens with trusted publishing).
-
Fix a minification regression with CSS media queries (#4315)
The previous release introduced support for parsing media queries which unintentionally introduced a regression with the removal of duplicate media rules during minification. Specifically the grammar for
@media <media-type> and <media-condition-without-or> { ... }was missing an equality check for the<media-condition-without-or>part, so rules with different suffix clauses in this position would incorrectly compare equal and be deduplicated. This release fixes the regression. -
Update the list of known JavaScript globals (#4310)
This release updates esbuild's internal list of known JavaScript globals. These are globals that are known to not have side-effects when the property is accessed. For example, accessing the global
Arrayproperty is considered to be side-effect free but accessing the globalscrollYproperty can trigger a layout, which is a side-effect. This is used by esbuild's tree-shaking to safely remove unused code that is known to be side-effect free. This update adds the following global properties:From ES2017:
AtomicsSharedArrayBuffer
From ES2020:
BigInt64ArrayBigUint64Array
From ES2021:
FinalizationRegistryWeakRef
From ES2025:
Float16ArrayIterator
Note that this does not indicate that constructing any of these objects is side-effect free, just that accessing the identifier is side-effect free. For example, this now allows esbuild to tree-shake classes that extend from
Iterator:// This can now be tree-shaken by esbuild: class ExampleIterator extends Iterator {} -
Add support for the new
@view-transitionCSS rule (#4313)With this release, esbuild now has improved support for pretty-printing and minifying the new
@view-transitionrule (which esbuild was previously unaware of):/* Original code */ @view-transition { navigation: auto; types: check; } /* Old output */ @view-transition { navigation: auto; types: check; } /* New output */ @view-transition { navigation: auto; types: check; }The new view transition feature provides a mechanism for creating animated transitions between documents in a multi-page app. You can read more about view transition rules here.
This change was contributed by @yisibl.
-
Trim CSS rules that will never match
The CSS minifier will now remove rules whose selectors contain
:is()and:where()as those selectors will never match. These selectors can currently be automatically generated by esbuild when you give esbuild nonsensical input such as the following:/* Original code */ div:before { color: green; &.foo { color: red; } } /* Old output (with --supported:nesting=false --minify) */ div:before{color:green}:is().foo{color:red} /* New output (with --supported:nesting=false --minify) */ div:before{color:green}This input is nonsensical because CSS nesting is (unfortunately) not supported inside of pseudo-elements such as
:before. Currently esbuild generates a rule containing:is()in this case when you tell esbuild to transform nested CSS into non-nested CSS. I think it's reasonable to do that as it sort of helps explain what's going on (or at least indicates that something is wrong in the output). It shouldn't be present in minified code, however, so this release now strips it out.
-
Add support for
with { type: 'bytes' }imports (#4292)The import bytes proposal has reached stage 2.7 in the TC39 process, which means that although it isn't quite recommended for implementation, it's generally approved and ready for validation. Furthermore it has already been implemented by Deno and Webpack. So with this release, esbuild will also add support for this. It behaves exactly the same as esbuild's existing
binaryloader. Here's an example:import data from './image.png' with { type: 'bytes' } const view = new DataView(data.buffer, 0, 24) const width = view.getInt32(16) const height = view.getInt32(20) console.log('size:', width + '\xD7' + height) -
Lower CSS media query range syntax (#3748, #4293)
With this release, esbuild will now transform CSS media query range syntax into equivalent syntax using
min-/max-prefixes for older browsers. For example, the following CSS:@media (640px <= width <= 960px) { main { display: flex; } }will be transformed like this with a target such as
--target=chrome100(or more specifically with--supported:media-range=falseif desired):@media (min-width: 640px) and (max-width: 960px) { main { display: flex; } }
-
Fix a panic in a minification edge case (#4287)
This release fixes a panic due to a null pointer that could happen when esbuild inlines a doubly-nested identity function and the final result is empty. It was fixed by emitting the value
undefinedin this case, which avoids the panic. This case must be rare since it hasn't come up until now. Here is an example of code that previously triggered the panic (which only happened when minifying):function identity(x) { return x } identity({ y: identity(123) }) -
Fix
@supportsnested inside pseudo-element (#4265)When transforming nested CSS to non-nested CSS, esbuild is supposed to filter out pseudo-elements such as
::placeholderfor correctness. The CSS nesting specification says the following:The nesting selector cannot represent pseudo-elements (identical to the behavior of the ':is()' pseudo-class). We’d like to relax this restriction, but need to do so simultaneously for both ':is()' and '&', since they’re intentionally built on the same underlying mechanisms.
However, it seems like this behavior is different for nested at-rules such as
@supports, which do work with pseudo-elements. So this release modifies esbuild's behavior to now take that into account:/* Original code */ ::placeholder { color: red; body & { color: green } @supports (color: blue) { color: blue } } /* Old output (with --supported:nesting=false) */ ::placeholder { color: red; } body :is() { color: green; } @supports (color: blue) { { color: blue; } } /* New output (with --supported:nesting=false) */ ::placeholder { color: red; } body :is() { color: green; } @supports (color: blue) { ::placeholder { color: blue; } }
-
Better support building projects that use Yarn on Windows (#3131, #3663)
With this release, you can now use esbuild to bundle projects that use Yarn Plug'n'Play on Windows on drives other than the
C:drive. The problem was as follows:- Yarn in Plug'n'Play mode on Windows stores its global module cache on the
C:drive - Some developers put their projects on the
D:drive - Yarn generates relative paths that use
../..to get from the project directory to the cache directory - Windows-style paths don't support directory traversal between drives via
..(soD:\..is justD:) - I didn't have access to a Windows machine for testing this edge case
Yarn works around this edge case by pretending Windows-style paths beginning with
C:\are actually Unix-style paths beginning with/C:/, so the../..path segments are able to navigate across drives inside Yarn's implementation. This was broken for a long time in esbuild but I finally got access to a Windows machine and was able to debug and fix this edge case. So you should now be able to bundle these projects with esbuild. - Yarn in Plug'n'Play mode on Windows stores its global module cache on the
-
Preserve parentheses around function expressions (#4252)
The V8 JavaScript VM uses parentheses around function expressions as an optimization hint to immediately compile the function. Otherwise the function would be lazily-compiled, which has additional overhead if that function is always called immediately as lazy compilation involves parsing the function twice. You can read V8's blog post about this for more details.
Previously esbuild did not represent parentheses around functions in the AST so they were lost during compilation. With this change, esbuild will now preserve parentheses around function expressions when they are present in the original source code. This means these optimization hints will not be lost when bundling with esbuild. In addition, esbuild will now automatically add this optimization hint to immediately-invoked function expressions. Here's an example:
// Original code const fn0 = () => 0 const fn1 = (() => 1) console.log(fn0, function() { return fn1() }()) // Old output const fn0 = () => 0; const fn1 = () => 1; console.log(fn0, function() { return fn1(); }()); // New output const fn0 = () => 0; const fn1 = (() => 1); console.log(fn0, (function() { return fn1(); })());Note that you do not want to wrap all function expressions in parentheses. This optimization hint should only be used for functions that are called on initial load. Using this hint for functions that are not called on initial load will unnecessarily delay the initial load. Again, see V8's blog post linked above for details.
-
Update Go from 1.23.10 to 1.23.12 (#4257, #4258)
This should have no effect on existing code as this version change does not change Go's operating system support. It may remove certain false positive reports (specifically CVE-2025-4674 and CVE-2025-47907) from vulnerability scanners that only detect which version of the Go compiler esbuild uses.
-
Fix another TypeScript parsing edge case (#4248)
This fixes a regression with a change in the previous release that tries to more accurately parse TypeScript arrow functions inside the
?:operator. The regression specifically involves parsing an arrow function containing a#privateidentifier inside the middle of a?:ternary operator inside a class body. This was fixed by propagating private identifier state into the parser clone used to speculatively parse the arrow function body. Here is an example of some affected code:class CachedDict { #has = (a: string) => dict.has(a); has = window ? (word: string): boolean => this.#has(word) : this.#has; } -
Fix a regression with the parsing of source phase imports
The change in the previous release to parse source phase imports failed to properly handle the following cases:
import source from 'bar' import source from from 'bar' import source type foo from 'bar'Parsing for these cases should now be fixed. The first case was incorrectly treated as a syntax error because esbuild was expecting the second case. And the last case was previously allowed but is now forbidden. TypeScript hasn't added this feature yet so it remains to be seen whether the last case will be allowed, but it's safer to disallow it for now. At least Babel doesn't allow the last case when parsing TypeScript, and Babel was involved with the source phase import specification.
-
Parse and print JavaScript imports with an explicit phase (#4238)
This release adds basic syntax support for the
deferandsourceimport phases in JavaScript:-
deferThis is a stage 3 proposal for an upcoming JavaScript feature that will provide one way to eagerly load but lazily initialize imported modules. The imported module is automatically initialized on first use. Support for this syntax will also be part of the upcoming release of TypeScript 5.9. The syntax looks like this:
import defer * as foo from "<specifier>"; const bar = await import.defer("<specifier>");Note that this feature deliberately cannot be used with the syntax
import defer foo from "<specifier>"orimport defer { foo } from "<specifier>". -
sourceThis is a stage 3 proposal for an upcoming JavaScript feature that will provide another way to eagerly load but lazily initialize imported modules. The imported module is returned in an uninitialized state. Support for this syntax may or may not be a part of TypeScript 5.9 (see this issue for details). The syntax looks like this:
import source foo from "<specifier>"; const bar = await import.source("<specifier>");Note that this feature deliberately cannot be used with the syntax
import defer * as foo from "<specifier>"orimport defer { foo } from "<specifier>".
This change only adds support for this syntax. These imports cannot currently be bundled by esbuild. To use these new features with esbuild's bundler, the imported paths must be external to the bundle and the output format must be set to
esm. -
-
Support optionally emitting absolute paths instead of relative paths (#338, #2082, #3023)
This release introduces the
--abs-paths=feature which takes a comma-separated list of situations where esbuild should use absolute paths instead of relative paths. There are currently three supported situations:code(comments and string literals),log(log message text and location info), andmetafile(the JSON build metadata).Using absolute paths instead of relative paths is not the default behavior because it means that the build results are no longer machine-independent (which means builds are no longer reproducible). Absolute paths can be useful when used with certain terminal emulators that allow you to click on absolute paths in the terminal text and/or when esbuild is being automatically invoked from several different directories within the same script.
-
Fix a TypeScript parsing edge case (#4241)
This release fixes an edge case with parsing an arrow function in TypeScript with a return type that's in the middle of a
?:ternary operator. For example:x = a ? (b) : c => d; y = a ? (b) : c => d : e;The
:token in the value assigned toxpairs with the?token, so it's not the start of a return type annotation. However, the first:token in the value assigned toyis the start of a return type annotation because after parsing the arrow function body, it turns out there's another:token that can be used to pair with the?token. This case is notable as it's the first TypeScript edge case that esbuild has needed a backtracking parser to parse. It has been addressed by a quick hack (cloning the whole parser) as it's a rare edge case and esbuild doesn't otherwise need a backtracking parser. Hopefully this is sufficient and doesn't cause any issues. -
Inline small constant strings when minifying
Previously esbuild's minifier didn't inline string constants because strings can be arbitrarily long, and this isn't necessarily a size win if the string is used more than once. Starting with this release, esbuild will now inline string constants when the length of the string is three code units or less. For example:
// Original code const foo = 'foo' console.log({ [foo]: true }) // Old output (with --minify --bundle --format=esm) var o="foo";console.log({[o]:!0}); // New output (with --minify --bundle --format=esm) console.log({foo:!0});Note that esbuild's constant inlining only happens in very restrictive scenarios to avoid issues with TDZ handling. This change doesn't change when esbuild's constant inlining happens. It only expands the scope of it to include certain string literals in addition to numeric and boolean literals.
-
Fix a memory leak when
cancel()is used on a build context (#4231)Calling
rebuild()followed bycancel()in rapid succession could previously leak memory. The bundler uses a producer/consumer model internally, and the resource leak was caused by the consumer being termianted while there were still remaining unreceived results from a producer. To avoid the leak, the consumer now waits for all producers to finish before terminating. -
Support empty
:is()and:where()syntax in CSS (#4232)Previously using these selectors with esbuild would generate a warning. That warning has been removed in this release for these cases.
-
Improve tree-shaking of
trystatements in dead code (#4224)With this release, esbuild will now remove certain
trystatements if esbuild considers them to be within dead code (i.e. code that is known to not ever be evaluated). For example:// Original code return 'foo' try { return 'bar' } catch {} // Old output (with --minify) return"foo";try{return"bar"}catch{} // New output (with --minify) return"foo"; -
Consider negated bigints to have no side effects
While esbuild currently considers
1,-1, and1nto all have no side effects, it didn't previously consider-1nto have no side effects. This is because esbuild does constant folding with numbers but not bigints. However, it meant that unused negative bigint constants were not tree-shaken. With this release, esbuild will now consider these expressions to also be side-effect free:// Original code let a = 1, b = -1, c = 1n, d = -1n // Old output (with --bundle --minify) (()=>{var n=-1n;})(); // New output (with --bundle --minify) (()=>{})(); -
Support a configurable delay in watch mode before rebuilding (#3476, #4178)
The
watch()API now takes adelayoption that lets you add a delay (in milliseconds) before rebuilding when a change is detected in watch mode. If you use a tool that regenerates multiple source files very slowly, this should make it more likely that esbuild's watch mode won't generate a broken intermediate build before the successful final build. This option is also available via the CLI using the--watch-delay=flag.This should also help avoid confusion about the
watch()API's options argument. It was previously empty to allow for future API expansion, which caused some people to think that the documentation was missing. It's no longer empty now that thewatch()API has an option. -
Allow mixed array for
entryPointsAPI option (#4223)The TypeScript type definitions now allow you to pass a mixed array of both string literals and object literals to the
entryPointsAPI option, such as['foo.js', { out: 'lib', in: 'bar.js' }]. This was always possible to do in JavaScript but the TypeScript type definitions were previously too restrictive. -
Update Go from 1.23.8 to 1.23.10 (#4204, #4207)
This should have no effect on existing code as this version change does not change Go's operating system support. It may remove certain false positive reports (specifically CVE-2025-4673 and CVE-2025-22874) from vulnerability scanners that only detect which version of the Go compiler esbuild uses.
-
Experimental support for esbuild on OpenHarmony (#4212)
With this release, esbuild now publishes the
@esbuild/openharmony-arm64npm package for OpenHarmony. It contains a WebAssembly binary instead of a native binary because Go doesn't currently support OpenHarmony. Node does support it, however, so in theory esbuild should now work on OpenHarmony through WebAssembly.This change was contributed by @hqzing.
-
Fix a regression with
browserinpackage.json(#4187)The fix to #4144 in version 0.25.3 introduced a regression that caused
browseroverrides specified inpackage.jsonto fail to override relative path names that end in a trailing slash. That behavior change affected theaxios@0.30.0package. This regression has been fixed, and now has test coverage. -
Add support for certain keywords as TypeScript tuple labels (#4192)
Previously esbuild could incorrectly fail to parse certain keywords as TypeScript tuple labels that are parsed by the official TypeScript compiler if they were followed by a
?modifier. These labels includedfunction,import,infer,new,readonly, andtypeof. With this release, these keywords will now be parsed correctly. Here's an example of some affected code:type Foo = [ value: any, readonly?: boolean, // This is now parsed correctly ] -
Add CSS prefixes for the
stretchsizing value (#4184)This release adds support for prefixing CSS declarations such as
div { width: stretch }. That CSS is now transformed into this depending on what the--target=setting includes:div { width: -webkit-fill-available; width: -moz-available; width: stretch; }
-
Add simple support for CORS to esbuild's development server (#4125)
Starting with version 0.25.0, esbuild's development server is no longer configured to serve cross-origin requests. This was a deliberate change to prevent any website you visit from accessing your running esbuild development server. However, this change prevented (by design) certain use cases such as "debugging in production" by having your production website load code from
localhostwhere the esbuild development server is running.To enable this use case, esbuild is adding a feature to allow Cross-Origin Resource Sharing (a.k.a. CORS) for simple requests. Specifically, passing your origin to the new
corsoption will now set theAccess-Control-Allow-Originresponse header when the request has a matchingOriginheader. Note that this currently only works for requests that don't send a preflightOPTIONSrequest, as esbuild's development server doesn't currently supportOPTIONSrequests.Some examples:
-
CLI:
esbuild --servedir=. --cors-origin=https://example.com -
JS:
const ctx = await esbuild.context({}) await ctx.serve({ servedir: '.', cors: { origin: 'https://example.com', }, }) -
Go:
ctx, _ := api.Context(api.BuildOptions{}) ctx.Serve(api.ServeOptions{ Servedir: ".", CORS: api.CORSOptions{ Origin: []string{"https://example.com"}, }, })
The special origin
*can be used to allow any origin to access esbuild's development server. Note that this means any website you visit will be able to read everything served by esbuild. -
-
Pass through invalid URLs in source maps unmodified (#4169)
This fixes a regression in version 0.25.0 where
sourcesin source maps that form invalid URLs were not being passed through to the output. Version 0.25.0 changed the interpretation ofsourcesfrom file paths to URLs, which means that URL parsing can now fail. Previously URLs that couldn't be parsed were replaced with the empty string. With this release, invalid URLs insourcesshould now be passed through unmodified. -
Handle exports named
__proto__in ES modules (#4162, #4163)In JavaScript, the special property name
__proto__sets the prototype when used inside an object literal. Previously esbuild's ESM-to-CommonJS conversion didn't special-case the property name of exports named__proto__so the exported getter accidentally became the prototype of the object literal. It's unclear what this affects, if anything, but it's better practice to avoid this by using a computed property name in this case.This fix was contributed by @magic-akari.
-
Fix lowered
asyncarrow functions beforesuper()(#4141, #4142)This change makes it possible to call an
asyncarrow function in a constructor before callingsuper()when targeting environments withoutasyncsupport, as long as the function body doesn't referencethis. Here's an example (notice the change fromthistonull):// Original code class Foo extends Object { constructor() { (async () => await foo())() super() } } // Old output (with --target=es2016) class Foo extends Object { constructor() { (() => __async(this, null, function* () { return yield foo(); }))(); super(); } } // New output (with --target=es2016) class Foo extends Object { constructor() { (() => __async(null, null, function* () { return yield foo(); }))(); super(); } }Some background: Arrow functions with the
asynckeyword are transformed into generator functions for older language targets such as--target=es2016. Since arrow functions capturethis, the generated code forwardsthisinto the body of the generator function. However, JavaScript class syntax forbids usingthisin a constructor before callingsuper(), and this forwarding was problematic since previously happened even when the function body doesn't usethis. Starting with this release, esbuild will now only forwardthisif it's used within the function body.This fix was contributed by @magic-akari.
-
Fix memory leak with
--watch=true(#4131, #4132)This release fixes a memory leak with esbuild when
--watch=trueis used instead of--watch. Previously using--watch=truecaused esbuild to continue to use more and more memory for every rebuild, but--watch=trueshould now behave like--watchand not leak memory.This bug happened because esbuild disables the garbage collector when it's not run as a long-lived process for extra speed, but esbuild's checks for which arguments cause esbuild to be a long-lived process weren't updated for the new
--watch=truestyle of boolean command-line flags. This has been an issue since this boolean flag syntax was added in version 0.14.24 in 2022. These checks are unfortunately separate from the regular argument parser because of how esbuild's internals are organized (the command-line interface is exposed as a separate Go API so you can build your own custom esbuild CLI).This fix was contributed by @mxschmitt.
-
More concise output for repeated legal comments (#4139)
Some libraries have many files and also use the same legal comment text in all files. Previously esbuild would copy each legal comment to the output file. Starting with this release, legal comments duplicated across separate files will now be grouped in the output file by unique comment content.
-
Allow a custom host with the development server (#4110)
With this release, you can now use a custom non-IP
hostwith esbuild's local development server (either with--serve=for the CLI or with theserve()call for the API). This was previously possible, but was intentionally broken in version 0.25.0 to fix a security issue. This change adds the functionality back except that it's now opt-in and only for a single domain name that you provide.For example, if you add a mapping in your
/etc/hostsfile fromlocal.example.comto127.0.0.1and then useesbuild --serve=local.example.com:8000, you will now be able to visit http://local.example.com:8000/ in your browser and successfully connect to esbuild's development server (doing that would previously have been blocked by the browser). This should also work with HTTPS if it's enabled (see esbuild's documentation for how to do that). -
Add a limit to CSS nesting expansion (#4114)
With this release, esbuild will now fail with an error if there is too much CSS nesting expansion. This can happen when nested CSS is converted to CSS without nesting for older browsers as expanding CSS nesting is inherently exponential due to the resulting combinatorial explosion. The expansion limit is currently hard-coded and cannot be changed, but is extremely unlikely to trigger for real code. It exists to prevent esbuild from using too much time and/or memory. Here's an example:
a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{color:red}}}}}}}}}}}}}}}}}}}}Previously, transforming this file with
--target=safari1took 5 seconds and generated 40mb of CSS. Trying to do that will now generate the following error instead:✘ [ERROR] CSS nesting is causing too much expansion example.css:1:60: 1 │ a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{a,b{color:red}}}}}}}}}}}}}}}}}}}} ╵ ^ CSS nesting expansion was terminated because a rule was generated with 65536 selectors. This limit exists to prevent esbuild from using too much time and/or memory. Please change your CSS to use fewer levels of nesting. -
Fix path resolution edge case (#4144)
This fixes an edge case where esbuild's path resolution algorithm could deviate from node's path resolution algorithm. It involves a confusing situation where a directory shares the same file name as a file (but without the file extension). See the linked issue for specific details. This appears to be a case where esbuild is correctly following node's published resolution algorithm but where node itself is doing something different. Specifically the step
LOAD_AS_FILEappears to be skipped when the input ends with... This release changes esbuild's behavior for this edge case to match node's behavior. -
Update Go from 1.23.7 to 1.23.8 (#4133, #4134)
This should have no effect on existing code as this version change does not change Go's operating system support. It may remove certain reports from vulnerability scanners that detect which version of the Go compiler esbuild uses, such as for CVE-2025-22871.
As a reminder, esbuild's development server is intended for development, not for production, so I do not consider most networking-related vulnerabilities in Go to be vulnerabilities in esbuild. Please do not use esbuild's development server in production.
-
Support flags in regular expressions for the API (#4121)
The JavaScript plugin API for esbuild takes JavaScript regular expression objects for the
filteroption. Internally these are translated into Go regular expressions. However, this translation previously ignored theflagsproperty of the regular expression. With this release, esbuild will now translate JavaScript regular expression flags into Go regular expression flags. Specifically the JavaScript regular expression/\.[jt]sx?$/iis turned into the Go regular expression`(?i)\.[jt]sx?$`internally inside of esbuild's API. This should make it possible to use JavaScript regular expressions with theiflag. Note that JavaScript and Go don't support all of the same regular expression features, so this mapping is only approximate. -
Fix node-specific annotations for string literal export names (#4100)
When node instantiates a CommonJS module, it scans the AST to look for names to expose via ESM named exports. This is a heuristic that looks for certain patterns such as
exports.NAME = ...ormodule.exports = { ... }. This behavior is used by esbuild to "annotate" CommonJS code that was converted from ESM with the original ESM export names. For example, when converting the fileexport let foo, barfrom ESM to CommonJS, esbuild appends this to the end of the file:// Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { bar, foo });However, this feature previously didn't work correctly for export names that are not valid identifiers, which can be constructed using string literal export names. The generated code contained a syntax error. That problem is fixed in this release:
// Original code let foo export { foo as "foo!" } // Old output (with --format=cjs --platform=node) ... 0 && (module.exports = { "foo!" }); // New output (with --format=cjs --platform=node) ... 0 && (module.exports = { "foo!": null }); -
Basic support for index source maps (#3439, #4109)
The source map specification has an optional mode called index source maps that makes it easier for tools to create an aggregate JavaScript file by concatenating many smaller JavaScript files with source maps, and then generate an aggregate source map by simply providing the original source maps along with some offset information. My understanding is that this is rarely used in practice. I'm only aware of two uses of it in the wild: ClojureScript and Turbopack.
This release provides basic support for indexed source maps. However, the implementation has not been tested on a real app (just on very simple test input). If you are using index source maps in a real app, please try this out and report back if anything isn't working for you.
Note that this is also not a complete implementation. For example, index source maps technically allows nesting source maps to an arbitrary depth, while esbuild's implementation in this release only supports a single level of nesting. It's unclear whether supporting more than one level of nesting is important or not given the lack of available test cases.
This feature was contributed by @clyfish.
-
Fix incorrect paths in inline source maps (#4070, #4075, #4105)
This fixes a regression from version 0.25.0 where esbuild didn't correctly resolve relative paths contained within source maps in inline
sourceMappingURLdata URLs. The paths were incorrectly being passed through as-is instead of being resolved relative to the source file containing thesourceMappingURLcomment, which was due to the data URL not being a file URL. This regression has been fixed, and this case now has test coverage. -
Fix invalid generated source maps (#4080, #4082, #4104, #4107)
This release fixes a regression from version 0.24.1 that could cause esbuild to generate invalid source maps. Specifically under certain conditions, esbuild could generate a mapping with an out-of-bounds source index. It was introduced by code that attempted to improve esbuild's handling of "null" entries in source maps (i.e. mappings with a generated position but no original position). This regression has been fixed.
This fix was contributed by @jridgewell.
-
Fix a regression with non-file source map paths (#4078)
The format of paths in source maps that aren't in the
filenamespace was unintentionally changed in version 0.25.0. Path namespaces is an esbuild-specific concept that is optionally available for plugins to use to distinguish paths fromfilepaths and from paths meant for other plugins. Previously the namespace was prepended to the path joined with a:character, but version 0.25.0 unintentionally failed to prepend the namespace. The previous behavior has been restored. -
Fix a crash with
switchoptimization (#4088)The new code in the previous release to optimize dead code in switch statements accidentally introduced a crash in the edge case where one or more switch case values include a function expression. This is because esbuild now visits the case values first to determine whether any cases are dead code, and then visits the case bodies once the dead code status is known. That triggered some internal asserts that guard against traversing the AST in an unexpected order. This crash has been fixed by changing esbuild to expect the new traversal ordering. Here's an example of affected code:
switch (x) { case '': return y.map(z => z.value) case y.map(z => z.key).join(','): return [] } -
Update Go from 1.23.5 to 1.23.7 (#4076, #4077)
This should have no effect on existing code as this version change does not change Go's operating system support. It may remove certain reports from vulnerability scanners that detect which version of the Go compiler esbuild uses.
This PR was contributed by @MikeWillCook.
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.24.0 or ~0.24.0. See npm's documentation about semver for more information.
-
Restrict access to esbuild's development server (GHSA-67mh-4wv8-2f99)
This change addresses esbuild's first security vulnerability report. Previously esbuild set the
Access-Control-Allow-Originheader to*to allow esbuild's development server to be flexible in how it's used for development. However, this allows the websites you visit to make HTTP requests to esbuild's local development server, which gives read-only access to your source code if the website were to fetch your source code's specific URL. You can read more information in the report.Starting with this release, CORS will now be disabled, and requests will now be denied if the host does not match the one provided to
--serve=. The default host is0.0.0.0, which refers to all of the IP addresses that represent the local machine (e.g. both127.0.0.1and192.168.0.1). If you want to customize anything about esbuild's development server, you can put a proxy in front of esbuild and modify the incoming and/or outgoing requests.In addition, the
serve()API call has been changed to return an array ofhostsinstead of a singlehoststring. This makes it possible to determine all of the hosts that esbuild's development server will accept.Thanks to @sapphi-red for reporting this issue.
-
Delete output files when a build fails in watch mode (#3643)
It has been requested for esbuild to delete files when a build fails in watch mode. Previously esbuild left the old files in place, which could cause people to not immediately realize that the most recent build failed. With this release, esbuild will now delete all output files if a rebuild fails. Fixing the build error and triggering another rebuild will restore all output files again.
-
Fix correctness issues with the CSS nesting transform (#3620, #3877, #3933, #3997, #4005, #4037, #4038)
This release fixes the following problems:
-
Naive expansion of CSS nesting can result in an exponential blow-up of generated CSS if each nesting level has multiple selectors. Previously esbuild sometimes collapsed individual nesting levels using
:is()to limit expansion. However, this collapsing wasn't correct in some cases, so it has been removed to fix correctness issues./* Original code */ .parent { > .a, > .b1 > .b2 { color: red; } } /* Old output (with --supported:nesting=false) */ .parent > :is(.a, .b1 > .b2) { color: red; } /* New output (with --supported:nesting=false) */ .parent > .a, .parent > .b1 > .b2 { color: red; }Thanks to @tim-we for working on a fix.
-
The
&CSS nesting selector can be repeated multiple times to increase CSS specificity. Previously esbuild ignored this possibility and incorrectly considered&&to have the same specificity as&. With this release, this should now work correctly:/* Original code (color should be red) */ div { && { color: red } & { color: blue } } /* Old output (with --supported:nesting=false) */ div { color: red; } div { color: blue; } /* New output (with --supported:nesting=false) */ div:is(div) { color: red; } div { color: blue; }Thanks to @CPunisher for working on a fix.
-
Previously transforming nested CSS incorrectly removed leading combinators from within pseudoclass selectors such as
:where(). This edge case has been fixed and how has test coverage./* Original code */ a b:has(> span) { a & { color: green; } } /* Old output (with --supported:nesting=false) */ a :is(a b:has(span)) { color: green; } /* New output (with --supported:nesting=false) */ a :is(a b:has(> span)) { color: green; }This fix was contributed by @NoremacNergfol.
-
The CSS minifier contains logic to remove the
&selector when it can be implied, which happens when there is only one and it's the leading token. However, this logic was incorrectly also applied to selector lists inside of pseudo-class selectors such as:where(). With this release, the minifier will now avoid applying this logic in this edge case:/* Original code */ .a { & .b { color: red } :where(& .b) { color: blue } } /* Old output (with --minify) */ .a{.b{color:red}:where(.b){color:#00f}} /* New output (with --minify) */ .a{.b{color:red}:where(& .b){color:#00f}}
-
-
Fix some correctness issues with source maps (#1745, #3183, #3613, #3982)
Previously esbuild incorrectly treated source map path references as file paths instead of as URLs. With this release, esbuild will now treat source map path references as URLs. This fixes the following problems with source maps:
-
File names in
sourceMappingURLthat contained a space previously did not encode the space as%20, which resulted in JavaScript tools (including esbuild) failing to read that path back in when consuming the generated output file. This should now be fixed. -
Absolute URLs in
sourceMappingURLthat use thefile://scheme previously attempted to read from a folder calledfile:. These URLs should now be recognized and parsed correctly. -
Entries in the
sourcesarray in the source map are now treated as URLs instead of file paths. The correct behavior for this is much more clear now that source maps has a formal specification. Many thanks to those who worked on the specification.
-
-
Fix incorrect package for
@esbuild/netbsd-arm64(#4018)Due to a copy+paste typo, the binary published to
@esbuild/netbsd-arm64was not actually forarm64, and didn't run in that environment. This release should fix running esbuild in that environment (NetBSD on 64-bit ARM). Sorry about the mistake. -
Fix a minification bug with bitwise operators and bigints (#4065)
This change removes an incorrect assumption in esbuild that all bitwise operators result in a numeric integer. That assumption was correct up until the introduction of bigints in ES2020, but is no longer correct because almost all bitwise operators now operate on both numbers and bigints. Here's an example of the incorrect minification:
// Original code if ((a & b) !== 0) found = true // Old output (with --minify) a&b&&(found=!0); // New output (with --minify) (a&b)!==0&&(found=!0); -
Fix esbuild incorrectly rejecting valid TypeScript edge case (#4027)
The following TypeScript code is valid:
export function open(async?: boolean): void { console.log(async as boolean) }Before this version, esbuild would fail to parse this with a syntax error as it expected the token sequence
async as ...to be the start of an async arrow function expressionasync as => .... This edge case should be parsed correctly by esbuild starting with this release. -
Transform BigInt values into constructor calls when unsupported (#4049)
Previously esbuild would refuse to compile the BigInt literals (such as
123n) if they are unsupported in the configured target environment (such as with--target=es6). The rationale was that they cannot be polyfilled effectively because they change the behavior of JavaScript's arithmetic operators and JavaScript doesn't have operator overloading.However, this prevents using esbuild with certain libraries that would otherwise work if BigInt literals were ignored, such as with old versions of the
bufferlibrary before the library fixed support for running in environments without BigInt support. So with this release, esbuild will now turn BigInt literals into BigInt constructor calls (so123nbecomesBigInt(123)) and generate a warning in this case. You can turn off the warning with--log-override:bigint=silentor restore the warning to an error with--log-override:bigint=errorif needed. -
Change how
consoleAPI dropping works (#4020)Previously the
--drop:consolefeature replaced all method calls off of theconsoleglobal withundefinedregardless of how long the property access chain was (so it applied toconsole.log()andconsole.log.call(console)andconsole.log.not.a.method()). However, it was pointed out that this breaks uses ofconsole.log.bind(console). That's also incompatible with Terser's implementation of the feature, which is where this feature originally came from (it does supportbind). So with this release, using this feature with esbuild will now only replace one level of method call (unless extended bycallorapply) and will replace the method being called with an empty function in complex cases:// Original code const x = console.log('x') const y = console.log.call(console, 'y') const z = console.log.bind(console)('z') // Old output (with --drop-console) const x = void 0; const y = void 0; const z = (void 0)("z"); // New output (with --drop-console) const x = void 0; const y = void 0; const z = (() => { }).bind(console)("z");This should more closely match Terser's existing behavior.
-
Allow BigInt literals as
definevaluesWith this release, you can now use BigInt literals as define values, such as with
--define:FOO=123n. Previously trying to do this resulted in a syntax error. -
Fix a bug with resolve extensions in
node_modules(#4053)The
--resolve-extensions=option lets you specify the order in which to try resolving implicit file extensions. For complicated reasons, esbuild reorders TypeScript file extensions after JavaScript ones inside ofnode_modulesso that JavaScript source code is always preferred to TypeScript source code inside of dependencies. However, this reordering had a bug that could accidentally change the relative order of TypeScript file extensions if one of them was a prefix of the other. That bug has been fixed in this release. You can see the issue for details. -
Better minification of statically-determined
switchcases (#4028)With this release, esbuild will now try to trim unused code within
switchstatements when the test expression andcaseexpressions are primitive literals. This can arise when the test expression is an identifier that is substituted for a primitive literal at compile time. For example:// Original code switch (MODE) { case 'dev': installDevToolsConsole() break case 'prod': return default: throw new Error } // Old output (with --minify '--define:MODE="prod"') switch("prod"){case"dev":installDevToolsConsole();break;case"prod":return;default:throw new Error} // New output (with --minify '--define:MODE="prod"') return; -
Emit
/* @__KEY__ */for string literals derived from property names (#4034)Property name mangling is an advanced feature that shortens certain property names for better minification (I say "advanced feature" because it's very easy to break your code with it). Sometimes you need to store a property name in a string, such as
obj.get('foo')instead ofobj.foo. JavaScript minifiers such as esbuild and Terser have a convention where a/* @__KEY__ */comment before the string makes it behave like a property name. Soobj.get(/* @__KEY__ */ 'foo')allows the contents of the string'foo'to be shortened.However, esbuild sometimes itself generates string literals containing property names when transforming code, such as when lowering class fields to ES6 or when transforming TypeScript decorators. Previously esbuild didn't generate its own
/* @__KEY__ */comments in this case, which means that minifying your code by running esbuild again on its own output wouldn't work correctly (this does not affect people that both minify and transform their code in a single step).With this release, esbuild will now generate
/* @__KEY__ */comments for property names in generated string literals. To avoid lots of unnecessary output for people that don't use this advanced feature, the generated comments will only be present when the feature is active. If you want to generate the comments but not actually mangle any property names, you can use a flag that has no effect such as--reserve-props=., which tells esbuild to not mangle any property names (but still activates this feature). -
The
textloader now strips the UTF-8 BOM if present (#3935)Some software (such as Notepad on Windows) can create text files that start with the three bytes
0xEF 0xBB 0xBF, which is referred to as the "byte order mark". This prefix is intended to be removed before using the text. Previously esbuild'stextloader included this byte sequence in the string, which turns into a prefix of\uFEFFin a JavaScript string when decoded from UTF-8. With this release, esbuild'stextloader will now remove these bytes when they occur at the start of the file. -
Omit legal comment output files when empty (#3670)
Previously configuring esbuild with
--legal-comment=externalor--legal-comment=linkedwould always generate a.LEGAL.txtoutput file even if it was empty. Starting with this release, esbuild will now only do this if the file will be non-empty. This should result in a more organized output directory in some cases. -
Update Go from 1.23.1 to 1.23.5 (#4056, #4057)
This should have no effect on existing code as this version change does not change Go's operating system support. It may remove certain reports from vulnerability scanners that detect which version of the Go compiler esbuild uses.
This PR was contributed by @MikeWillCook.
-
Allow passing a port of 0 to the development server (#3692)
Unix sockets interpret a port of 0 to mean "pick a random unused port in the ephemeral port range". However, esbuild's default behavior when the port is not specified is to pick the first unused port starting from 8000 and upward. This is more convenient because port 8000 is typically free, so you can for example restart the development server and reload your app in the browser without needing to change the port in the URL. Since esbuild is written in Go (which does not have optional fields like JavaScript), not specifying the port in Go means it defaults to 0, so previously passing a port of 0 to esbuild caused port 8000 to be picked.
Starting with this release, passing a port of 0 to esbuild when using the CLI or the JS API will now pass port 0 to the OS, which will pick a random ephemeral port. To make this possible, the
Portoption in the Go API has been changed fromuint16toint(to allow for additional sentinel values) and passing a port of -1 in Go now picks a random port. Both the CLI and JS APIs now remap an explicitly-provided port of 0 into -1 for the internal Go API.Another option would have been to change
Portin Go fromuint16to*uint16(Go's closest equivalent ofnumber | undefined). However, that would make the common case of providing an explicit port in Go very awkward as Go doesn't support taking the address of integer constants. This tradeoff isn't worth it as picking a random ephemeral port is a rare use case. So the CLI and JS APIs should now match standard Unix behavior when the port is 0, but you need to use -1 instead with Go API. -
Minification now avoids inlining constants with direct
eval(#4055)Direct
evalcan be used to introduce a new variable like this:const variable = false ;(function () { eval("var variable = true") console.log(variable) })()Previously esbuild inlined
variablehere (which becamefalse), which changed the behavior of the code. This inlining is now avoided, but please keep in mind that directevalbreaks many assumptions that JavaScript tools hold about normal code (especially when bundling) and I do not recommend using it. There are usually better alternatives that have a more localized impact on your code. You can read more about this here: https://esbuild.github.io/link/direct-eval/
-
Fix regression with
--defineandimport.meta(#4010, #4012, #4013)The previous change in version 0.24.1 to use a more expression-like parser for
definevalues to allow quoted property names introduced a regression that removed the ability to use--define:import.meta=.... Even thoughimportis normally a keyword that can't be used as an identifier, ES modules special-case theimport.metaexpression to behave like an identifier anyway. This change fixes the regression.This fix was contributed by @sapphi-red.
-
Allow
es2024as a target intsconfig.json(#4004)TypeScript recently added
es2024as a compilation target, so esbuild now supports this in thetargetfield oftsconfig.jsonfiles, such as in the following configuration file:{ "compilerOptions": { "target": "ES2024" } }As a reminder, the only thing that esbuild uses this field for is determining whether or not to use legacy TypeScript behavior for class fields. You can read more in the documentation.
This fix was contributed by @billyjanitsch.
-
Allow automatic semicolon insertion after
get/setThis change fixes a grammar bug in the parser that incorrectly treated the following code as a syntax error:
class Foo { get *x() {} set *y() {} }The above code will be considered valid starting with this release. This change to esbuild follows a similar change to TypeScript which will allow this syntax starting with TypeScript 5.7.
-
Allow quoted property names in
--defineand--pure(#4008)The
defineandpureAPI options now accept identifier expressions containing quoted property names. Previously all identifiers in the identifier expression had to be bare identifiers. This change now makes--defineand--pureconsistent with--global-name, which already supported quoted property names. For example, the following is now possible:// The following code now transforms to "return true;\n" console.log(esbuild.transformSync( `return process.env['SOME-TEST-VAR']`, { define: { 'process.env["SOME-TEST-VAR"]': 'true' } }, ))Note that if you're passing values like this on the command line using esbuild's
--defineflag, then you'll need to know how to escape quote characters for your shell. You may find esbuild's JavaScript API more ergonomic and portable than writing shell code. -
Minify empty
try/catch/finallyblocks (#4003)With this release, esbuild will now attempt to minify empty
tryblocks:// Original code try {} catch { foo() } finally { bar() } // Old output (with --minify) try{}catch{foo()}finally{bar()} // New output (with --minify) bar();This can sometimes expose additional minification opportunities.
-
Include
entryPointmetadata for thecopyloader (#3985)Almost all entry points already include a
entryPointfield in theoutputsmap in esbuild's build metadata. However, this wasn't the case for thecopyloader as that loader is a special-case that doesn't behave like other loaders. This release adds theentryPointfield in this case. -
Source mappings may now contain
nullentries (#3310, #3878)With this change, sources that result in an empty source map may now emit a
nullsource mapping (i.e. one with a generated position but without a source index or original position). This change improves source map accuracy by fixing a problem where minified code from a source without any source mappings could potentially still be associated with a mapping from another source file earlier in the generated output on the same minified line. It manifests as nonsensical files in source mapped stack traces. Now thenullmapping "resets" the source map so that any lookups into the minified code without any mappings resolves tonull(which appears as the output file in stack traces) instead of the incorrect source file.This change shouldn't affect anything in most situations. I'm only mentioning it in the release notes in case it introduces a bug with source mapping. It's part of a work-in-progress future feature that will let you omit certain unimportant files from the generated source map to reduce source map size.
-
Avoid using the parent directory name for determinism (#3998)
To make generated code more readable, esbuild includes the name of the source file when generating certain variable names within the file. Specifically bundling a CommonJS file generates a variable to store the lazily-evaluated module initializer. However, if a file is named
index.js(or with a different extension), esbuild will use the name of the parent directory instead for a better name (since many packages have files all namedindex.jsbut have unique directory names).This is problematic when the bundle entry point is named
index.jsand the parent directory name is non-deterministic (e.g. a temporary directory created by a build script). To avoid non-determinism in esbuild's output, esbuild will now useindexinstead of the parent directory in this case. Specifically this will happen if the parent directory is equal to esbuild'soutbaseAPI option, which defaults to the lowest common ancestor of all user-specified entry point paths. -
Experimental support for esbuild on NetBSD (#3974)
With this release, esbuild now has a published binary executable for NetBSD in the
@esbuild/netbsd-arm64npm package, and esbuild's installer has been modified to attempt to use it when on NetBSD. Hopefully this makes installing esbuild via npm work on NetBSD. This change was contributed by @bsiegert.⚠️ Note: NetBSD is not one of Node's supported platforms, so installing esbuild may or may not work on NetBSD depending on how Node has been patched. This is not a problem with esbuild. ⚠️
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.23.0 or ~0.23.0. See npm's documentation about semver for more information.
-
Drop support for older platforms (#3902)
This release drops support for the following operating system:
- macOS 10.15 Catalina
This is because the Go programming language dropped support for this operating system version in Go 1.23, and this release updates esbuild from Go 1.22 to Go 1.23. Go 1.23 now requires macOS 11 Big Sur or later.
Note that this only affects the binary esbuild executables that are published to the esbuild npm package. It's still possible to compile esbuild's source code for these older operating systems. If you need to, you can compile esbuild for yourself using an older version of the Go compiler (before Go version 1.23). That might look something like this:
git clone https://github.com/evanw/esbuild.git cd esbuild go build ./cmd/esbuild ./esbuild --version -
Fix class field decorators in TypeScript if
useDefineForClassFieldsisfalse(#3913)Setting the
useDefineForClassFieldsflag tofalseintsconfig.jsonmeans class fields use the legacy TypeScript behavior instead of the standard JavaScript behavior. Specifically they use assign semantics instead of define semantics (e.g. setters are triggered) and fields without an initializer are not initialized at all. However, when this legacy behavior is combined with standard JavaScript decorators, TypeScript switches to always initializing all fields, even those without initializers. Previously esbuild incorrectly continued to omit field initializers for this edge case. These field initializers in this case should now be emitted starting with this release. -
Avoid incorrect cycle warning with
tsconfig.jsonmultiple inheritance (#3898)TypeScript 5.0 introduced multiple inheritance for
tsconfig.jsonfiles whereextendscan be an array of file paths. Previously esbuild would incorrectly treat files encountered more than once when processing separate subtrees of the multiple inheritance hierarchy as an inheritance cycle. With this release,tsconfig.jsonfiles containing this edge case should work correctly without generating a warning. -
Handle Yarn Plug'n'Play stack overflow with
tsconfig.json(#3915)Previously a
tsconfig.jsonfile thatextendsanother file in a package with anexportsmap could cause a stack overflow when Yarn's Plug'n'Play resolution was active. This edge case should work now starting with this release. -
Work around more issues with Deno 1.31+ (#3917)
This version of Deno broke the
stdinandstdoutproperties on command objects for inherited streams, which matters when you run esbuild's Deno module as the entry point (i.e. whenimport.meta.mainistrue). Previously esbuild would crash in Deno 1.31+ if you ran esbuild like that. This should be fixed starting with this release.This fix was contributed by @Joshix-1.
-
Allow using the
node:import prefix withes*targets (#3821)The
node:prefix on imports is an alternate way to import built-in node modules. For example,import fs from "fs"can also be writtenimport fs from "node:fs". This only works with certain newer versions of node, so esbuild removes it when you target older versions of node such as with--target=node14so that your code still works. With the way esbuild's platform-specific feature compatibility table works, this was added by saying that only newer versions of node support this feature. However, that means that a target such as--target=node18,es2022removes thenode:prefix because none of thees*targets are known to support this feature. This release adds the support for thenode:flag to esbuild's internal compatibility table fores*to allow you to use compound targets like this:// Original code import fs from 'node:fs' fs.open // Old output (with --bundle --format=esm --platform=node --target=node18,es2022) import fs from "fs"; fs.open; // New output (with --bundle --format=esm --platform=node --target=node18,es2022) import fs from "node:fs"; fs.open; -
Fix a panic when using the CLI with invalid build flags if
--analyzeis present (#3834)Previously esbuild's CLI could crash if it was invoked with flags that aren't valid for a "build" API call and the
--analyzeflag is present. This was caused by esbuild's internals attempting to add a Go plugin (which is how--analyzeis implemented) to a null build object. The panic has been fixed in this release. -
Fix incorrect location of certain error messages (#3845)
This release fixes a regression that caused certain errors relating to variable declarations to be reported at an incorrect location. The regression was introduced in version 0.18.7 of esbuild.
-
Print comments before case clauses in switch statements (#3838)
With this release, esbuild will attempt to print comments that come before case clauses in switch statements. This is similar to what esbuild already does for comments inside of certain types of expressions. Note that these types of comments are not printed if minification is enabled (specifically whitespace minification).
-
Fix a memory leak with
pluginData(#3825)With this release, the build context's internal
pluginDatacache will now be cleared when starting a new build. This should fix a leak of memory from plugins that returnpluginDataobjects fromonResolveand/oronLoadcallbacks.
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.22.0 or ~0.22.0. See npm's documentation about semver for more information.
-
Revert the recent change to avoid bundling dependencies for node (#3819)
This release reverts the recent change in version 0.22.0 that made
--packages=externalthe default behavior with--platform=node. The default is now back to--packages=bundle.I've just been made aware that Amazon doesn't pin their dependencies in their "AWS CDK" product, which means that whenever esbuild publishes a new release, many people (potentially everyone?) using their SDK around the world instantly starts using it without Amazon checking that it works first. This change in version 0.22.0 happened to break their SDK. I'm amazed that things haven't broken before this point. This revert attempts to avoid these problems for Amazon's customers. Hopefully Amazon will pin their dependencies in the future.
In addition, this is probably a sign that esbuild is used widely enough that it now needs to switch to a more complicated release model. I may have esbuild use a beta channel model for further development.
-
Fix preserving collapsed JSX whitespace (#3818)
When transformed, certain whitespace inside JSX elements is ignored completely if it collapses to an empty string. However, the whitespace should only be ignored if the JSX is being transformed, not if it's being preserved. This release fixes a bug where esbuild was previously incorrectly ignoring collapsed whitespace with
--jsx=preserve. Here is an example:// Original code <Foo> <Bar /> </Foo> // Old output (with --jsx=preserve) <Foo><Bar /></Foo>; // New output (with --jsx=preserve) <Foo> <Bar /> </Foo>;
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.21.0 or ~0.21.0. See npm's documentation about semver for more information.
-
Omit packages from bundles by default when targeting node (#1874, #2830, #2846, #2915, #3145, #3294, #3323, #3582, #3809, #3815)
This breaking change is an experiment. People are commonly confused when using esbuild to bundle code for node (i.e. for
--platform=node) because some packages may not be intended for bundlers, and may use node-specific features that don't work with a bundler. Even though esbuild's "getting started" instructions say to use--packages=externalto work around this problem, many people don't read the documentation and don't do this, and are then confused when it doesn't work. So arguably this is a bad default behavior for esbuild to have if people keep tripping over this.With this release, esbuild will now omit packages from the bundle by default when the platform is
node(i.e. the previous behavior of--packages=externalis now the default in this case). Note that your dependencies must now be present on the file system when your bundle is run. If you don't want this behavior, you can do--packages=bundleto allow packages to be included in the bundle (i.e. the previous default behavior). Note that--packages=bundledoesn't mean all packages are bundled, just that packages are allowed to be bundled. You can still exclude individual packages from the bundle using--external:even when--packages=bundleis present.The
--packages=setting considers all import paths that "look like" package imports in the original source code to be package imports. Specifically import paths that don't start with a path segment of/or.or..are considered to be package imports. The only two exceptions to this rule are subpath imports (which start with a#character) and TypeScript path remappings viapathsand/orbaseUrlintsconfig.json(which are applied first). -
Drop support for older platforms (#3802)
This release drops support for the following operating systems:
- Windows 7
- Windows 8
- Windows Server 2008
- Windows Server 2012
This is because the Go programming language dropped support for these operating system versions in Go 1.21, and this release updates esbuild from Go 1.20 to Go 1.22.
Note that this only affects the binary esbuild executables that are published to the
esbuildnpm package. It's still possible to compile esbuild's source code for these older operating systems. If you need to, you can compile esbuild for yourself using an older version of the Go compiler (before Go version 1.21). That might look something like this:git clone https://github.com/evanw/esbuild.git cd esbuild go build ./cmd/esbuild ./esbuild.exe --versionIn addition, this release increases the minimum required node version for esbuild's JavaScript API from node 12 to node 18. Node 18 is the oldest version of node that is still being supported (see node's release schedule for more information). This increase is because of an incompatibility between the JavaScript that the Go compiler generates for the
esbuild-wasmpackage and versions of node before node 17.4 (specifically thecrypto.getRandomValuesfunction). -
Update
await usingbehavior to match TypeScriptTypeScript 5.5 subtly changes the way
await usingbehaves. This release updates esbuild to match these changes in TypeScript. You can read more about these changes in microsoft/TypeScript#58624. -
Allow
es2024as a target environmentThe ECMAScript 2024 specification was just approved, so it has been added to esbuild as a possible compilation target. You can read more about the features that it adds here: https://2ality.com/2024/06/ecmascript-2024.html. The only addition that's relevant for esbuild is the regular expression
/vflag. With--target=es2024, regular expressions that use the/vflag will now be passed through untransformed instead of being transformed into a call tonew RegExp. -
Publish binaries for OpenBSD on 64-bit ARM (#3665, #3674)
With this release, you should now be able to install the
esbuildnpm package in OpenBSD on 64-bit ARM, such as on an Apple device with an M1 chip.This was contributed by @ikmckenz.
-
Publish binaries for WASI (WebAssembly System Interface) preview 1 (#3300, #3779)
The upcoming WASI (WebAssembly System Interface) standard is going to be a way to run WebAssembly outside of a JavaScript host environment. In this scenario you only need a
.wasmfile without any supporting JavaScript code. Instead of JavaScript providing the APIs for the host environment, the WASI standard specifies a "system interface" that WebAssembly code can access directly (e.g. for file system access).Development versions of the WASI specification are being released using preview numbers. The people behind WASI are currently working on preview 2 but the Go compiler has released support for preview 1, which from what I understand is now considered an unsupported legacy release. However, some people have requested that esbuild publish binary executables that support WASI preview 1 so they can experiment with them.
This release publishes esbuild precompiled for WASI preview 1 to the
@esbuild/wasi-preview1package on npm (specifically the file@esbuild/wasi-preview1/esbuild.wasm). This binary executable has not been tested and won't be officially supported, as it's for an old preview release of a specification that has since moved in another direction. If it works for you, great! If not, then you'll likely have to wait for the ecosystem to evolve before using esbuild with WASI. For example, it sounds like perhaps WASI preview 1 doesn't include support for opening network sockets so esbuild's local development server is unlikely to work with WASI preview 1. -
Warn about
onResolveplugins not setting a path (#3790)Plugins that return values from
onResolvewithout resolving the path (i.e. without setting eitherpathorexternal: true) will now cause a warning. This is because esbuild only uses return values fromonResolveif it successfully resolves the path, and it's not good for invalid input to be silently ignored. -
Add a new Go API for running the CLI with plugins (#3539)
With esbuild's Go API, you can now call
cli.RunWithPlugins(args, plugins)to pass an array of esbuild plugins to be used during the build process. This allows you to create a CLI that behaves similarly to esbuild's CLI but with additional Go plugins enabled.This was contributed by @edewit.
-
Fix
Symbol.metadataon classes without a class decorator (#3781)This release fixes a bug with esbuild's support for the decorator metadata proposal. Previously esbuild only added the
Symbol.metadataproperty to decorated classes if there was a decorator on the class element itself. However, the proposal says that theSymbol.metadataproperty should be present on all classes that have any decorators at all, not just those with a decorator on the class element itself. -
Allow unknown import attributes to be used with the
copyloader (#3792)Import attributes (the
withkeyword onimportstatements) are allowed to alter how that path is loaded. For example, esbuild cannot assume that it knows how to load./bagel.jsas typebagel:// This is an error with "--bundle" without also using "--external:./bagel.js" import tasty from "./bagel.js" with { type: "bagel" }Because of that, bundling this code with esbuild is an error unless the file
./bagel.jsis external to the bundle (such as with--bundle --external:./bagel.js).However, there is an additional case where it's ok for esbuild to allow this: if the file is loaded using the
copyloader. That's because thecopyloader behaves similarly to--externalin that the file is left external to the bundle. The difference is that thecopyloader copies the file into the output folder and rewrites the import path while--externaldoesn't. That means the following will now work with thecopyloader (such as with--bundle --loader:.bagel=copy):// This is no longer an error with "--bundle" and "--loader:.bagel=copy" import tasty from "./tasty.bagel" with { type: "bagel" } -
Support import attributes with glob-style imports (#3797)
This release adds support for import attributes (the
withoption) to glob-style imports (dynamic imports with certain string literal patterns as paths). These imports previously didn't support import attributes due to an oversight. So code like this will now work correctly:async function loadLocale(locale: string): Locale { const data = await import(`./locales/${locale}.data`, { with: { type: 'json' } }) return unpackLocale(locale, data) }Previously this didn't work even though esbuild normally supports forcing the JSON loader using an import attribute. Attempting to do this used to result in the following error:
✘ [ERROR] No loader is configured for ".data" files: locales/en-US.data example.ts:2:28: 2 │ const data = await import(`./locales/${locale}.data`, { with: { type: 'json' } }) ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~In addition, this change means plugins can now access the contents of
withfor glob-style imports. -
Support
${configDir}intsconfig.jsonfiles (#3782)This adds support for a new feature from the upcoming TypeScript 5.5 release. The character sequence
${configDir}is now respected at the start ofbaseUrlandpathsvalues, which are used by esbuild during bundling to correctly map import paths to file system paths. This feature lets basetsconfig.jsonfiles specified viaextendsrefer to the directory of the top-leveltsconfig.jsonfile. Here is an example:{ "compilerOptions": { "paths": { "js/*": ["${configDir}/dist/js/*"] } } }You can read more in TypeScript's blog post about their upcoming 5.5 release. Note that this feature does not make use of template literals (you need to use
"${configDir}/dist/js/*"not`${configDir}/dist/js/*`). The syntax fortsconfig.jsonis still just JSON with comments, and JSON syntax does not allow template literals. This feature only recognizes${configDir}in strings for certain path-like properties, and only at the beginning of the string. -
Fix internal error with
--supported:object-accessors=false(#3794)This release fixes a regression in 0.21.0 where some code that was added to esbuild's internal runtime library of helper functions for JavaScript decorators fails to parse when you configure esbuild with
--supported:object-accessors=false. The reason is that esbuild introduced code that does{ get [name]() {} }which uses both theobject-extensionsfeature for the[name]and theobject-accessorsfeature for theget, but esbuild was incorrectly only checking forobject-extensionsand not forobject-accessors. Additional tests have been added to avoid this type of issue in the future. A workaround for this issue in earlier releases is to also add--supported:object-extensions=false.
-
Update support for import assertions and import attributes in node (#3778)
Import assertions (the
assertkeyword) have been removed from node starting in v22.0.0. So esbuild will now strip them and generate a warning with--target=node22or above:▲ [WARNING] The "assert" keyword is not supported in the configured target environment ("node22") [assert-to-with] example.mjs:1:40: 1 │ import json from "esbuild/package.json" assert { type: "json" } │ ~~~~~~ ╵ with Did you mean to use "with" instead of "assert"?Import attributes (the
withkeyword) have been backported to node 18 starting in v18.20.0. So esbuild will no longer strip them with--target=node18.NifNis 20 or greater. -
Fix
for awaittransform when a label is presentThis release fixes a bug where the
for awaittransform, which wraps the loop in atrystatement, previously failed to also move the loop's label into thetrystatement. This bug only affects code that uses both of these features in combination. Here's an example of some affected code:// Original code async function test() { outer: for await (const x of [Promise.resolve([0, 1])]) { for (const y of x) if (y) break outer throw 'fail' } } // Old output (with --target=es6) function test() { return __async(this, null, function* () { outer: try { for (var iter = __forAwait([Promise.resolve([0, 1])]), more, temp, error; more = !(temp = yield iter.next()).done; more = false) { const x = temp.value; for (const y of x) if (y) break outer; throw "fail"; } } catch (temp) { error = [temp]; } finally { try { more && (temp = iter.return) && (yield temp.call(iter)); } finally { if (error) throw error[0]; } } }); } // New output (with --target=es6) function test() { return __async(this, null, function* () { try { outer: for (var iter = __forAwait([Promise.resolve([0, 1])]), more, temp, error; more = !(temp = yield iter.next()).done; more = false) { const x = temp.value; for (const y of x) if (y) break outer; throw "fail"; } } catch (temp) { error = [temp]; } finally { try { more && (temp = iter.return) && (yield temp.call(iter)); } finally { if (error) throw error[0]; } } }); } -
Do additional constant folding after cross-module enum inlining (#3416, #3425)
This release adds a few more cases where esbuild does constant folding after cross-module enum inlining.
// Original code: enum.ts export enum Platform { WINDOWS = 'windows', MACOS = 'macos', LINUX = 'linux', } // Original code: main.ts import { Platform } from './enum'; declare const PLATFORM: string; export function logPlatform() { if (PLATFORM == Platform.WINDOWS) console.log('Windows'); else if (PLATFORM == Platform.MACOS) console.log('macOS'); else if (PLATFORM == Platform.LINUX) console.log('Linux'); else console.log('Other'); } // Old output (with --bundle '--define:PLATFORM="macos"' --minify --format=esm) function n(){"windows"=="macos"?console.log("Windows"):"macos"=="macos"?console.log("macOS"):"linux"=="macos"?console.log("Linux"):console.log("Other")}export{n as logPlatform}; // New output (with --bundle '--define:PLATFORM="macos"' --minify --format=esm) function n(){console.log("macOS")}export{n as logPlatform}; -
Pass import attributes to on-resolve plugins (#3384, #3639, #3646)
With this release, on-resolve plugins will now have access to the import attributes on the import via the
withproperty of the arguments object. This mirrors thewithproperty of the arguments object that's already passed to on-load plugins. In addition, you can now passwithto theresolve()API call which will then forward that value on to all relevant plugins. Here's an example of a plugin that can now be written:const examplePlugin = { name: 'Example plugin', setup(build) { build.onResolve({ filter: /.*/ }, args => { if (args.with.type === 'external') return { external: true } }) } } require('esbuild').build({ stdin: { contents: ` import foo from "./foo" with { type: "external" } foo() `, }, bundle: true, format: 'esm', write: false, plugins: [examplePlugin], }).then(result => { console.log(result.outputFiles[0].text) }) -
Formatting support for the
@position-tryrule (#3773)Chrome shipped this new CSS at-rule in version 125 as part of the CSS anchor positioning API. With this release, esbuild now knows to expect a declaration list inside of the
@position-trybody block and will format it appropriately. -
Always allow internal string import and export aliases (#3343)
Import and export names can be string literals in ES2022+. Previously esbuild forbid any usage of these aliases when the target was below ES2022. Starting with this release, esbuild will only forbid such usage when the alias would otherwise end up in output as a string literal. String literal aliases that are only used internally in the bundle and are "compiled away" are no longer errors. This makes it possible to use string literal aliases with esbuild's
injectfeature even when the target is earlier than ES2022.
-
Implement the decorator metadata proposal (#3760)
This release implements the decorator metadata proposal, which is a sub-proposal of the decorators proposal. Microsoft shipped the decorators proposal in TypeScript 5.0 and the decorator metadata proposal in TypeScript 5.2, so it's important that esbuild also supports both of these features. Here's a quick example:
// Shim the "Symbol.metadata" symbol Symbol.metadata ??= Symbol('Symbol.metadata') const track = (_, context) => { (context.metadata.names ||= []).push(context.name) } class Foo { @track foo = 1 @track bar = 2 } // Prints ["foo", "bar"] console.log(Foo[Symbol.metadata].names)⚠️ WARNING ⚠️
This proposal has been marked as "stage 3" which means "recommended for implementation". However, it's still a work in progress and isn't a part of JavaScript yet, so keep in mind that any code that uses JavaScript decorator metadata may need to be updated as the feature continues to evolve. If/when that happens, I will update esbuild's implementation to match the specification. I will not be supporting old versions of the specification.
-
Fix bundled decorators in derived classes (#3768)
In certain cases, bundling code that uses decorators in a derived class with a class body that references its own class name could previously generate code that crashes at run-time due to an incorrect variable name. This problem has been fixed. Here is an example of code that was compiled incorrectly before this fix:
class Foo extends Object { @(x => x) foo() { return Foo } } console.log(new Foo().foo()) -
Fix
tsconfig.jsonfiles inside symlinked directories (#3767)This release fixes an issue with a scenario involving a
tsconfig.jsonfile thatextendsanother file from within a symlinked directory that uses thepathsfeature. In that case, the implicitbaseURLvalue should be based on the real path (i.e. after expanding all symbolic links) instead of the original path. This was already done for other files that esbuild resolves but was not yet done fortsconfig.jsonbecause it's special-cased (the regular path resolver can't be used because the information insidetsconfig.jsonis involved in path resolution). Note that this fix no longer applies if the--preserve-symlinkssetting is enabled.
-
Correct
thisin field and accessor decorators (#3761)This release changes the value of
thisin initializers for class field and accessor decorators from the module-levelthisvalue to the appropriatethisvalue for the decorated element (either the class or the instance). It was previously incorrect due to lack of test coverage. Here's an example of a decorator that doesn't work without this change:const dec = () => function() { this.bar = true } class Foo { @dec static foo } console.log(Foo.bar) // Should be "true" -
Allow
es2023as a target environment (#3762)TypeScript recently added
es2023as a compilation target, so esbuild now supports this too. There is no difference between a target ofes2022andes2023as far as esbuild is concerned since the 2023 edition of JavaScript doesn't introduce any new syntax features.
-
Fix a regression with
--keep-names(#3756)The previous release introduced a regression with the
--keep-namessetting and object literals withget/setaccessor methods, in which case the generated code contained syntax errors. This release fixes the regression:// Original code x = { get y() {} } // Output from version 0.21.0 (with --keep-names) x = { get y: /* @__PURE__ */ __name(function() { }, "y") }; // Output from this version (with --keep-names) x = { get y() { } };
This release doesn't contain any deliberately-breaking changes. However, it contains a very complex new feature and while all of esbuild's tests pass, I would not be surprised if an important edge case turns out to be broken. So I'm releasing this as a breaking change release to avoid causing any trouble. As usual, make sure to test your code when you upgrade.
-
Implement the JavaScript decorators proposal (#104)
With this release, esbuild now contains an implementation of the upcoming JavaScript decorators proposal. This is the same feature that shipped in TypeScript 5.0 and has been highly-requested on esbuild's issue tracker. You can read more about them in that blog post and in this other (now slightly outdated) extensive blog post here: https://2ality.com/2022/10/javascript-decorators.html. Here's a quick example:
const log = (fn, context) => function() { console.log(`before ${context.name}`) const it = fn.apply(this, arguments) console.log(`after ${context.name}`) return it } class Foo { @log static foo() { console.log('in foo') } } // Logs "before foo", "in foo", "after foo" Foo.foo()Note that this feature is different than the existing "TypeScript experimental decorators" feature that esbuild already implements. It uses similar syntax but behaves very differently, and the two are not compatible (although it's sometimes possible to write decorators that work with both). TypeScript experimental decorators will still be supported by esbuild going forward as they have been around for a long time, are very widely used, and let you do certain things that are not possible with JavaScript decorators (such as decorating function parameters). By default esbuild will parse and transform JavaScript decorators, but you can tell esbuild to parse and transform TypeScript experimental decorators instead by setting
"experimentalDecorators": truein yourtsconfig.jsonfile.Probably at least half of the work for this feature went into creating a test suite that exercises many of the proposal's edge cases: https://github.com/evanw/decorator-tests. It has given me a reasonable level of confidence that esbuild's initial implementation is acceptable. However, I don't have access to a significant sample of real code that uses JavaScript decorators. If you're currently using JavaScript decorators in a real code base, please try out esbuild's implementation and let me know if anything seems off.
⚠️ WARNING ⚠️
This proposal has been in the works for a very long time (work began around 10 years ago in 2014) and it is finally getting close to becoming part of the JavaScript language. However, it's still a work in progress and isn't a part of JavaScript yet, so keep in mind that any code that uses JavaScript decorators may need to be updated as the feature continues to evolve. The decorators proposal is pretty close to its final form but it can and likely will undergo some small behavioral adjustments before it ends up becoming a part of the standard. If/when that happens, I will update esbuild's implementation to match the specification. I will not be supporting old versions of the specification.
-
Optimize the generated code for private methods
Previously when lowering private methods for old browsers, esbuild would generate one
WeakSetfor each private method. This mirrors similar logic for generating oneWeakSetfor each private field. Using a separateWeakMapfor private fields is necessary as their assignment can be observable:let it class Bar { constructor() { it = this } } class Foo extends Bar { #x = 1 #y = null.foo static check() { console.log(#x in it, #y in it) } } try { new Foo } catch {} Foo.check()This prints
true falsebecause this partially-initialized instance has#xbut not#y. In other words, it's not true that all class instances will always have all of their private fields. However, the assignment of private methods to a class instance is not observable. In other words, it's true that all class instances will always have all of their private methods. This means esbuild can lower private methods into code where all methods share a singleWeakSet, which is smaller, faster, and uses less memory. Other JavaScript processing tools such as the TypeScript compiler already make this optimization. Here's what this change looks like:// Original code class Foo { #x() { return this.#x() } #y() { return this.#y() } #z() { return this.#z() } } // Old output (--supported:class-private-method=false) var _x, x_fn, _y, y_fn, _z, z_fn; class Foo { constructor() { __privateAdd(this, _x); __privateAdd(this, _y); __privateAdd(this, _z); } } _x = new WeakSet(); x_fn = function() { return __privateMethod(this, _x, x_fn).call(this); }; _y = new WeakSet(); y_fn = function() { return __privateMethod(this, _y, y_fn).call(this); }; _z = new WeakSet(); z_fn = function() { return __privateMethod(this, _z, z_fn).call(this); }; // New output (--supported:class-private-method=false) var _Foo_instances, x_fn, y_fn, z_fn; class Foo { constructor() { __privateAdd(this, _Foo_instances); } } _Foo_instances = new WeakSet(); x_fn = function() { return __privateMethod(this, _Foo_instances, x_fn).call(this); }; y_fn = function() { return __privateMethod(this, _Foo_instances, y_fn).call(this); }; z_fn = function() { return __privateMethod(this, _Foo_instances, z_fn).call(this); }; -
Fix an obscure bug with lowering class members with computed property keys
When class members that use newer syntax features are transformed for older target environments, they sometimes need to be relocated. However, care must be taken to not reorder any side effects caused by computed property keys. For example, the following code must evaluate
a()thenb()thenc():class Foo { [a()]() {} [b()]; static { c() } }Previously esbuild did this by shifting the computed property key forward to the next spot in the evaluation order. Classes evaluate all computed keys first and then all static class elements, so if the last computed key needs to be shifted, esbuild previously inserted a static block at start of the class body, ensuring it came before all other static class elements:
var _a; class Foo { constructor() { __publicField(this, _a); } static { _a = b(); } [a()]() { } static { c(); } }However, this could cause esbuild to accidentally generate a syntax error if the computed property key contains code that isn't allowed in a static block, such as an
awaitexpression. With this release, esbuild fixes this problem by shifting the computed property key backward to the previous spot in the evaluation order instead, which may push it into theextendsclause or even before the class itself:// Original code class Foo { [a()]() {} [await b()]; static { c() } } // Old output (with --supported:class-field=false) var _a; class Foo { constructor() { __publicField(this, _a); } static { _a = await b(); } [a()]() { } static { c(); } } // New output (with --supported:class-field=false) var _a, _b; class Foo { constructor() { __publicField(this, _a); } [(_b = a(), _a = await b(), _b)]() { } static { c(); } } -
Fix some
--keep-namesedge casesThe
NamedEvaluationsyntax-directed operation in the JavaScript specification gives certain anonymous expressions anameproperty depending on where they are in the syntax tree. For example, the following initializers convey anamevalue:var foo = function() {} var bar = class {} console.log(foo.name, bar.name)When you enable esbuild's
--keep-namessetting, esbuild generates additional code to represent thisNamedEvaluationoperation so that the value of thenameproperty persists even when the identifiers are renamed (e.g. due to minification).However, I recently learned that esbuild's implementation of
NamedEvaluationis missing a few cases. Specifically esbuild was missing property definitions, class initializers, logical-assignment operators. These cases should now all be handled:var obj = { foo: function() {} } class Foo0 { foo = function() {} } class Foo1 { static foo = function() {} } class Foo2 { accessor foo = function() {} } class Foo3 { static accessor foo = function() {} } foo ||= function() {} foo &&= function() {} foo ??= function() {}
-
Support TypeScript experimental decorators on
abstractclass fields (#3684)With this release, you can now use TypeScript experimental decorators on
abstractclass fields. This was silently compiled incorrectly in esbuild 0.19.7 and below, and was an error from esbuild 0.19.8 to esbuild 0.20.1. Code such as the following should now work correctly:// Original code const log = (x: any, y: string) => console.log(y) abstract class Foo { @log abstract foo: string } new class extends Foo { foo = '' } // Old output (with --loader=ts --tsconfig-raw={\"compilerOptions\":{\"experimentalDecorators\":true}}) const log = (x, y) => console.log(y); class Foo { } new class extends Foo { foo = ""; }(); // New output (with --loader=ts --tsconfig-raw={\"compilerOptions\":{\"experimentalDecorators\":true}}) const log = (x, y) => console.log(y); class Foo { } __decorateClass([ log ], Foo.prototype, "foo", 2); new class extends Foo { foo = ""; }(); -
JSON loader now preserves
__proto__properties (#3700)Copying JSON source code into a JavaScript file will change its meaning if a JSON object contains the
__proto__key. A literal__proto__property in a JavaScript object literal sets the prototype of the object instead of adding a property named__proto__, while a literal__proto__property in a JSON object literal just adds a property named__proto__. With this release, esbuild will now work around this problem by converting JSON to JavaScript with a computed property key in this case:// Original code import data from 'data:application/json,{"__proto__":{"fail":true}}' if (Object.getPrototypeOf(data)?.fail) throw 'fail' // Old output (with --bundle) (() => { // <data:application/json,{"__proto__":{"fail":true}}> var json_proto_fail_true_default = { __proto__: { fail: true } }; // entry.js if (Object.getPrototypeOf(json_proto_fail_true_default)?.fail) throw "fail"; })(); // New output (with --bundle) (() => { // <data:application/json,{"__proto__":{"fail":true}}> var json_proto_fail_true_default = { ["__proto__"]: { fail: true } }; // example.mjs if (Object.getPrototypeOf(json_proto_fail_true_default)?.fail) throw "fail"; })(); -
Improve dead code removal of
switchstatements (#3659)With this release, esbuild will now remove
switchstatements in branches when minifying if they are known to never be evaluated:// Original code if (true) foo(); else switch (bar) { case 1: baz(); break } // Old output (with --minify) if(1)foo();else switch(bar){case 1:} // New output (with --minify) foo(); -
Empty enums should behave like an object literal (#3657)
TypeScript allows you to create an empty enum and add properties to it at run time. While people usually use an empty object literal for this instead of a TypeScript enum, esbuild's enum transform didn't anticipate this use case and generated
undefinedinstead of{}for an empty enum. With this release, you can now use an empty enum to generate an empty object literal.// Original code enum Foo {} // Old output (with --loader=ts) var Foo = /* @__PURE__ */ ((Foo2) => { })(Foo || {}); // New output (with --loader=ts) var Foo = /* @__PURE__ */ ((Foo2) => { return Foo2; })(Foo || {}); -
Handle Yarn Plug'n'Play edge case with
tsconfig.json(#3698)Previously a
tsconfig.jsonfile thatextendsanother file in a package with anexportsmap failed to work when Yarn's Plug'n'Play resolution was active. This edge case should work now starting with this release. -
Work around issues with Deno 1.31+ (#3682)
Version 0.20.0 of esbuild changed how the esbuild child process is run in esbuild's API for Deno. Previously it used
Deno.runbut that API is being removed in favor ofDeno.Command. As part of this change, esbuild is now calling the newunreffunction on esbuild's long-lived child process, which is supposed to allow Deno to exit when your code has finished running even though the child process is still around (previously you had to explicitly call esbuild'sstop()function to terminate the child process for Deno to be able to exit).However, this introduced a problem for Deno's testing API which now fails some tests that use esbuild with
error: Promise resolution is still pending but the event loop has already resolved. It's unclear to me why this is happening. The call tounrefwas recommended by someone on the Deno core team, and calling Node's equivalentunrefAPI has been working fine for esbuild in Node for a long time. It could be that I'm using it incorrectly, or that there's some reference counting and/or garbage collection bug in Deno's internals, or that Deno'sunrefjust works differently than Node'sunref. In any case, it's not good for Deno tests that use esbuild to be failing.In this release, I am removing the call to
unrefto fix this issue. This means that you will now have to call esbuild'sstop()function to allow Deno to exit, just like you did before esbuild version 0.20.0 when this regression was introduced.Note: This regression wasn't caught earlier because Deno doesn't seem to fail tests that have outstanding
setTimeoutcalls, which esbuild's test harness was using to enforce a maximum test runtime. Adding asetTimeoutwas allowing esbuild's Deno tests to succeed. So this regression doesn't necessarily apply to all people using tests in Deno.
-
Fix a bug with the CSS nesting transform (#3648)
This release fixes a bug with the CSS nesting transform for older browsers where the generated CSS could be incorrect if a selector list contained a pseudo element followed by another selector. The bug was caused by incorrectly mutating the parent rule's selector list when filtering out pseudo elements for the child rules:
/* Original code */ .foo { &:after, & .bar { color: red; } } /* Old output (with --supported:nesting=false) */ .foo .bar, .foo .bar { color: red; } /* New output (with --supported:nesting=false) */ .foo:after, .foo .bar { color: red; } -
Constant folding for JavaScript inequality operators (#3645)
This release introduces constant folding for the
< > <= >=operators. The minifier will now replace these operators withtrueorfalsewhen both sides are compile-time numeric or string constants:// Original code console.log(1 < 2, '🍕' > '🧀') // Old output (with --minify) console.log(1<2,"🍕">"🧀"); // New output (with --minify) console.log(!0,!1); -
Better handling of
__proto__edge cases (#3651)JavaScript object literal syntax contains a special case where a non-computed property with a key of
__proto__sets the prototype of the object. This does not apply to computed properties or to properties that use the shorthand property syntax introduced in ES6. Previously esbuild didn't correctly preserve the "sets the prototype" status of properties inside an object literal, meaning a property that sets the prototype could accidentally be transformed into one that doesn't and vice versa. This has now been fixed:// Original code function foo(__proto__) { return { __proto__: __proto__ } // Note: sets the prototype } function bar(__proto__, proto) { { let __proto__ = proto return { __proto__ } // Note: doesn't set the prototype } } // Old output function foo(__proto__) { return { __proto__ }; // Note: no longer sets the prototype (WRONG) } function bar(__proto__, proto) { { let __proto__2 = proto; return { __proto__: __proto__2 }; // Note: now sets the prototype (WRONG) } } // New output function foo(__proto__) { return { __proto__: __proto__ }; // Note: sets the prototype (correct) } function bar(__proto__, proto) { { let __proto__2 = proto; return { ["__proto__"]: __proto__2 }; // Note: doesn't set the prototype (correct) } } -
Fix cross-platform non-determinism with CSS color space transformations (#3650)
The Go compiler takes advantage of "fused multiply and add" (FMA) instructions on certain processors which do the operation
x*y + zwithout intermediate rounding. This causes esbuild's CSS color space math to differ on different processors (currentlyppc64leands390x), which breaks esbuild's guarantee of deterministic output. To avoid this, esbuild's color space math now inserts afloat64()cast around every single math operation. This tells the Go compiler not to use the FMA optimization. -
Fix a crash when resolving a path from a directory that doesn't exist (#3634)
This release fixes a regression where esbuild could crash when resolving an absolute path if the source directory for the path resolution operation doesn't exist. While this situation doesn't normally come up, it could come up when running esbuild concurrently with another operation that mutates the file system as esbuild is doing a build (such as using
gitto switch branches). The underlying problem was a regression that was introduced in version 0.18.0.
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.19.0 or ~0.19.0. See npm's documentation about semver for more information.
This time there is only one breaking change, and it only matters for people using Deno. Deno tests that use esbuild will now fail unless you make the change described below.
-
Work around API deprecations in Deno 1.40.x (#3609, #3611)
Deno 1.40.0 was just released and introduced run-time warnings about certain APIs that esbuild uses. With this release, esbuild will work around these run-time warnings by using newer APIs if they are present and falling back to the original APIs otherwise. This should avoid the warnings without breaking compatibility with older versions of Deno.
Unfortunately, doing this introduces a breaking change. The newer child process APIs lack a way to synchronously terminate esbuild's child process, so calling
esbuild.stop()from within a Deno test is no longer sufficient to prevent Deno from failing a test that uses esbuild's API (Deno fails tests that create a child process without killing it before the test ends). To work around this, esbuild'sstop()function has been changed to return a promise, and you now have to changeesbuild.stop()toawait esbuild.stop()in all of your Deno tests. -
Reorder implicit file extensions within
node_modules(#3341, #3608)In version 0.18.0, esbuild changed the behavior of implicit file extensions within
node_modulesdirectories (i.e. in published packages) to prefer.jsover.tseven when the--resolve-extensions=order prefers.tsover.js(which it does by default). However, doing that also accidentally made esbuild prefer.cssover.ts, which caused problems for people that published packages containing both TypeScript and CSS in files with the same name.With this release, esbuild will reorder TypeScript file extensions immediately after the last JavaScript file extensions in the implicit file extension order instead of putting them at the end of the order. Specifically the default implicit file extension order is
.tsx,.ts,.jsx,.js,.css,.jsonwhich used to become.jsx,.js,.css,.json,.tsx,.tsinnode_modulesdirectories. With this release it will now become.jsx,.js,.tsx,.ts,.css,.jsoninstead.Why even rewrite the implicit file extension order at all? One reason is because the
.jsfile is more likely to behave correctly than the.tsfile. The behavior of the.tsfile may depend ontsconfig.jsonand thetsconfig.jsonfile may not even be published, or may useextendsto refer to a basetsconfig.jsonfile that wasn't published. People can get into this situation when they forget to add all.tsfiles to their.npmignorefile before publishing to npm. Picking.jsover.tshelps make it more likely that resulting bundle will behave correctly.
-
The "preserve" JSX mode now preserves JSX text verbatim (#3605)
The JSX specification deliberately doesn't specify how JSX text is supposed to be interpreted and there is no canonical way to interpret JSX text. Two most popular interpretations are Babel and TypeScript. Yes they are different (esbuild deliberately follows TypeScript by the way).
Previously esbuild normalized text to the TypeScript interpretation when the "preserve" JSX mode is active. However, "preserve" should arguably reproduce the original JSX text verbatim so that whatever JSX transform runs after esbuild is free to interpret it however it wants. So with this release, esbuild will now pass JSX text through unmodified:
// Original code let el = <a href={'/'} title=''"'> some text {foo} more text </a> // Old output (with --loader=jsx --jsx=preserve) let el = <a href="/" title={`'"`}> {" some text"} {foo} {"more text "} </a>; // New output (with --loader=jsx --jsx=preserve) let el = <a href={"/"} title=''"'> some text {foo} more text </a>; -
Allow JSX elements as JSX attribute values
JSX has an obscure feature where you can use JSX elements in attribute position without surrounding them with
{...}. It looks like this:let el = <div data-ab=<><a/><b/></>/>;I think I originally didn't implement it even though it's part of the JSX specification because it previously didn't work in TypeScript (and potentially also in Babel?). However, support for it was silently added in TypeScript 4.8 without me noticing and Babel has also since fixed their bugs regarding this feature. So I'm adding it to esbuild too now that I know it's widely supported.
Keep in mind that there is some ongoing discussion about removing this feature from JSX. I agree that the syntax seems out of place (it does away with the elegance of "JSX is basically just XML with
{...}escapes" for something arguably harder to read, which doesn't seem like a good trade-off), but it's in the specification and TypeScript and Babel both implement it so I'm going to have esbuild implement it too. However, I reserve the right to remove it from esbuild if it's ever removed from the specification in the future. So use it with caution. -
Fix a bug with TypeScript type parsing (#3574)
This release fixes a bug with esbuild's TypeScript parser where a conditional type containing a union type that ends with an infer type that ends with a constraint could fail to parse. This was caused by the "don't parse a conditional type" flag not getting passed through the union type parser. Here's an example of valid TypeScript code that previously failed to parse correctly:
type InferUnion<T> = T extends { a: infer U extends number } | infer U extends number ? U : never
-
Fix TypeScript-specific class transform edge case (#3559)
The previous release introduced an optimization that avoided transforming
super()in the class constructor for TypeScript code compiled withuseDefineForClassFieldsset tofalseif all class instance fields have no initializers. The rationale was that in this case, all class instance fields are omitted in the output so no changes to the constructor are needed. However, if all of this is the case and there are#privateinstance fields with initializers, those private instance field initializers were still being moved into the constructor. This was problematic because they were being inserted before the call tosuper()(sincesuper()is now no longer transformed in that case). This release introduces an additional optimization that avoids moving the private instance field initializers into the constructor in this edge case, which generates smaller code, matches the TypeScript compiler's output more closely, and avoids this bug:// Original code class Foo extends Bar { #private = 1; public: any; constructor() { super(); } } // Old output (with esbuild v0.19.9) class Foo extends Bar { constructor() { super(); this.#private = 1; } #private; } // Old output (with esbuild v0.19.10) class Foo extends Bar { constructor() { this.#private = 1; super(); } #private; } // New output class Foo extends Bar { #private = 1; constructor() { super(); } } -
Minifier: allow reording a primitive past a side-effect (#3568)
The minifier previously allowed reordering a side-effect past a primitive, but didn't handle the case of reordering a primitive past a side-effect. This additional case is now handled:
// Original code function f() { let x = false; let y = x; const boolean = y; let frag = $.template(`<p contenteditable="${boolean}">hello world</p>`); return frag; } // Old output (with --minify) function f(){const e=!1;return $.template(`<p contenteditable="${e}">hello world</p>`)} // New output (with --minify) function f(){return $.template('<p contenteditable="false">hello world</p>')} -
Minifier: consider properties named using known
Symbolinstances to be side-effect free (#3561)Many things in JavaScript can have side effects including property accesses and ToString operations, so using a symbol such as
Symbol.iteratoras a computed property name is not obviously side-effect free. This release adds a special case for knownSymbolinstances so that they are considered side-effect free when used as property names. For example, this class declaration will now be considered side-effect free:class Foo { *[Symbol.iterator]() { } } -
Provide the
stop()API in node to exit esbuild's child process (#3558)You can now call
stop()in esbuild's node API to exit esbuild's child process to reclaim the resources used. It only makes sense to do this for a long-lived node process when you know you will no longer be making any more esbuild API calls. It is not necessary to call this to allow node to exit, and it's advantageous to not call this in between calls to esbuild's API as sharing a single long-lived esbuild child process is more efficient than re-creating a new esbuild child process for every API call. This API call used to exist but was removed in version 0.9.0. This release adds it back due to a user request.
-
Fix glob imports in TypeScript files (#3319)
This release fixes a problem where bundling a TypeScript file containing a glob import could emit a call to a helper function that doesn't exist. The problem happened because esbuild's TypeScript transformation removes unused imports (which is required for correctness, as they may be type-only imports) and esbuild's glob import transformation wasn't correctly marking the imported helper function as used. This wasn't caught earlier because most of esbuild's glob import tests were written in JavaScript, not in TypeScript.
-
Fix
require()glob imports with bundling disabled (#3546)Previously
require()calls containing glob imports were incorrectly transformed when bundling was disabled. All glob imports should only be transformed when bundling is enabled. This bug has been fixed. -
Fix a panic when transforming optional chaining with
define(#3551, #3554)This release fixes a case where esbuild could crash with a panic, which was triggered by using
defineto replace an expression containing an optional chain. Here is an example:// Original code console.log(process?.env.SHELL) // Old output (with --define:process.env={}) /* panic: Internal error (while parsing "<stdin>") */ // New output (with --define:process.env={}) var define_process_env_default = {}; console.log(define_process_env_default.SHELL);This fix was contributed by @hi-ogawa.
-
Work around a bug in node's CommonJS export name detector (#3544)
The export names of a CommonJS module are dynamically-determined at run time because CommonJS exports are properties on a mutable object. But the export names of an ES module are statically-determined at module instantiation time by using
importandexportsyntax and cannot be changed at run time.When you import a CommonJS module into an ES module in node, node scans over the source code to attempt to detect the set of export names that the CommonJS module will end up using. That statically-determined set of names is used as the set of names that the ES module is allowed to import at module instantiation time. However, this scan appears to have bugs (or at least, can cause false positives) because it doesn't appear to do any scope analysis. Node will incorrectly consider the module to export something even if the assignment is done to a local variable instead of to the module-level
exportsobject. For example:// confuseNode.js exports.confuseNode = function(exports) { // If this local is called "exports", node incorrectly // thinks this file has an export called "notAnExport". exports.notAnExport = function() { }; };You can see that node incorrectly thinks the file
confuseNode.jshas an export callednotAnExportwhen that file is loaded in an ES module context:$ node -e 'import("./confuseNode.js").then(console.log)' [Module: null prototype] { confuseNode: [Function (anonymous)], default: { confuseNode: [Function (anonymous)] }, notAnExport: undefined }To avoid this, esbuild will now rename local variables that use the names
exportsandmodulewhen generating CommonJS output for thenodeplatform. -
Fix the return value of esbuild's
super()shim (#3538)Some people write
constructormethods that use the return value ofsuper()instead of usingthis. This isn't too common because TypeScript doesn't let you do that but it can come up when writing JavaScript. Previously esbuild's class lowering transform incorrectly transformed the return value ofsuper()intoundefined. With this release, the return value ofsuper()will now bethisinstead:// Original code class Foo extends Object { field constructor() { console.log(typeof super()) } } new Foo // Old output (with --target=es6) class Foo extends Object { constructor() { var __super = (...args) => { super(...args); __publicField(this, "field"); }; console.log(typeof __super()); } } new Foo(); // New output (with --target=es6) class Foo extends Object { constructor() { var __super = (...args) => { super(...args); __publicField(this, "field"); return this; }; console.log(typeof __super()); } } new Foo(); -
Terminate the Go GC when esbuild's
stop()API is called (#3552)If you use esbuild with WebAssembly and pass the
worker: falseflag toesbuild.initialize(), then esbuild will run the WebAssembly module on the main thread. If you do this within a Deno test and that test callsesbuild.stop()to clean up esbuild's resources, Deno may complain that asetTimeout()call lasted past the end of the test. This happens when the Go is in the middle of a garbage collection pass and has scheduled additional ongoing garbage collection work. Normally callingesbuild.stop()will terminate the web worker that the WebAssembly module runs in, which will terminate the Go GC, but that doesn't happen if you disable the web worker withworker: false.With this release, esbuild will now attempt to terminate the Go GC in this edge case by calling
clearTimeout()on these pending timeouts. -
Apply
/* @__NO_SIDE_EFFECTS__ */on tagged template literals (#3511)Tagged template literals that reference functions annotated with a
@__NO_SIDE_EFFECTS__comment are now able to be removed via tree-shaking if the result is unused. This is a convention from Rollup. Here is an example:// Original code const html = /* @__NO_SIDE_EFFECTS__ */ (a, ...b) => ({ a, b }) html`<a>remove</a>` x = html`<b>keep</b>` // Old output (with --tree-shaking=true) const html = /* @__NO_SIDE_EFFECTS__ */ (a, ...b) => ({ a, b }); html`<a>remove</a>`; x = html`<b>keep</b>`; // New output (with --tree-shaking=true) const html = /* @__NO_SIDE_EFFECTS__ */ (a, ...b) => ({ a, b }); x = html`<b>keep</b>`;Note that this feature currently only works within a single file, so it's not especially useful. This feature does not yet work across separate files. I still recommend using
@__PURE__annotations instead of this feature, as they have wider tooling support. The drawback of course is that@__PURE__annotations need to be added at each call site, not at the declaration, and for non-call expressions such as template literals you need to wrap the expression in an IIFE (immediately-invoked function expression) to create a call expression to apply the@__PURE__annotation to. -
Publish builds for IBM AIX PowerPC 64-bit (#3549)
This release publishes a binary executable to npm for IBM AIX PowerPC 64-bit, which means that in theory esbuild can now be installed in that environment with
npm install esbuild. This hasn't actually been tested yet. If you have access to such a system, it would be helpful to confirm whether or not doing this actually works.
-
Add support for transforming new CSS gradient syntax for older browsers
The specification called CSS Images Module Level 4 introduces new CSS gradient syntax for customizing how the browser interpolates colors in between color stops. You can now control the color space that the interpolation happens in as well as (for "polar" color spaces) control whether hue angle interpolation happens clockwise or counterclockwise. You can read more about this in Mozilla's blog post about new CSS gradient features.
With this release, esbuild will now automatically transform this syntax for older browsers in the
targetlist. For example, here's a gradient that should appear as a rainbow in a browser that supports this new syntax:/* Original code */ .rainbow-gradient { width: 100px; height: 100px; background: linear-gradient(in hsl longer hue, #7ff, #77f); } /* New output (with --target=chrome99) */ .rainbow-gradient { width: 100px; height: 100px; background: linear-gradient( #77ffff, #77ffaa 12.5%, #77ff80 18.75%, #84ff77 21.88%, #99ff77 25%, #eeff77 37.5%, #fffb77 40.62%, #ffe577 43.75%, #ffbb77 50%, #ff9077 56.25%, #ff7b77 59.38%, #ff7788 62.5%, #ff77dd 75%, #ff77f2 78.12%, #f777ff 81.25%, #cc77ff 87.5%, #7777ff); }You can now use this syntax in your CSS source code and esbuild will automatically convert it to an equivalent gradient for older browsers. In addition, esbuild will now also transform "double position" and "transition hint" syntax for older browsers as appropriate:
/* Original code */ .stripes { width: 100px; height: 100px; background: linear-gradient(#e65 33%, #ff2 33% 67%, #99e 67%); } .glow { width: 100px; height: 100px; background: radial-gradient(white 10%, 20%, black); } /* New output (with --target=chrome33) */ .stripes { width: 100px; height: 100px; background: linear-gradient( #e65 33%, #ff2 33%, #ff2 67%, #99e 67%); } .glow { width: 100px; height: 100px; background: radial-gradient( #ffffff 10%, #aaaaaa 12.81%, #959595 15.62%, #7b7b7b 21.25%, #5a5a5a 32.5%, #444444 43.75%, #323232 55%, #161616 77.5%, #000000); }You can see visual examples of these new syntax features by looking at esbuild's gradient transformation tests.
If necessary, esbuild will construct a new gradient that approximates the original gradient by recursively splitting the interval in between color stops until the approximation error is within a small threshold. That is why the above output CSS contains many more color stops than the input CSS.
Note that esbuild deliberately replaces the original gradient with the approximation instead of inserting the approximation before the original gradient as a fallback. The latest version of Firefox has multiple gradient rendering bugs (including incorrect interpolation of partially-transparent colors and interpolating non-sRGB colors using the incorrect color space). If esbuild didn't replace the original gradient, then Firefox would use the original gradient instead of the fallback the appearance would be incorrect in Firefox. In other words, the latest version of Firefox supports modern gradient syntax but interprets it incorrectly.
-
Add support for
color(),lab(),lch(),oklab(),oklch(), andhwb()in CSSCSS has recently added lots of new ways of specifying colors. You can read more about this in Chrome's blog post about CSS color spaces.
This release adds support for minifying colors that use the
color(),lab(),lch(),oklab(),oklch(), orhwb()syntax and/or transforming these colors for browsers that don't support it yet:/* Original code */ div { color: hwb(90deg 20% 40%); background: color(display-p3 1 0 0); } /* New output (with --target=chrome99) */ div { color: #669933; background: #ff0f0e; background: color(display-p3 1 0 0); }As you can see, colors outside of the sRGB color space such as
color(display-p3 1 0 0)are mapped back into the sRGB gamut and inserted as a fallback for browsers that don't support the new color syntax. -
Allow empty type parameter lists in certain cases (#3512)
TypeScript allows interface declarations and type aliases to have empty type parameter lists. Previously esbuild didn't handle this edge case but with this release, esbuild will now parse this syntax:
interface Foo<> {} type Bar<> = {}This fix was contributed by @magic-akari.
-
Add a treemap chart to esbuild's bundle analyzer (#2848)
The bundler analyzer on esbuild's website (https://esbuild.github.io/analyze/) now has a treemap chart type in addition to the two existing chart types (sunburst and flame). This should be more familiar for people coming from other similar tools, as well as make better use of large screens.
-
Allow decorators after the
exportkeyword (#104)Previously esbuild's decorator parser followed the original behavior of TypeScript's experimental decorators feature, which only allowed decorators to come before the
exportkeyword. However, the upcoming JavaScript decorators feature also allows decorators to come after theexportkeyword. And with TypeScript 5.0, TypeScript now also allows experimental decorators to come after theexportkeyword too. So esbuild now allows this as well:// This old syntax has always been permitted: @decorator export class Foo {} @decorator export default class Foo {} // This new syntax is now permitted too: export @decorator class Foo {} export default @decorator class Foo {}In addition, esbuild's decorator parser has been rewritten to fix several subtle and likely unimportant edge cases with esbuild's parsing of exports and decorators in TypeScript (e.g. TypeScript apparently does automatic semicolon insertion after
interfaceandexport interfacebut not afterexport default interface). -
Pretty-print decorators using the same whitespace as the original
When printing code containing decorators, esbuild will now try to respect whether the original code contained newlines after the decorator or not. This can make generated code containing many decorators much more compact to read:
// Original code class Foo { @a @b @c abc @x @y @z xyz } // Old output class Foo { @a @b @c abc; @x @y @z xyz; } // New output class Foo { @a @b @c abc; @x @y @z xyz; }
-
Add support for bundling code that uses import attributes (#3384)
JavaScript is gaining new syntax for associating a map of string key-value pairs with individual ESM imports. The proposal is still a work in progress and is still undergoing significant changes before being finalized. However, the first iteration has already been shipping in Chromium-based browsers for a while, and the second iteration has landed in V8 and is now shipping in node, so it makes sense for esbuild to support it. Here are the two major iterations of this proposal (so far):
-
Import assertions (deprecated, will not be standardized)
- Uses the
assertkeyword - Does not affect module resolution
- Causes an error if the assertion fails
- Shipping in Chrome 91+ (and in esbuild 0.11.22+)
- Uses the
-
Import attributes (currently set to become standardized)
- Uses the
withkeyword - Affects module resolution
- Unknown attributes cause an error
- Shipping in node 21+
- Uses the
You can already use esbuild to bundle code that uses import assertions (the first iteration). However, this feature is mostly useless for bundlers because import assertions are not allowed to affect module resolution. It's basically only useful as an annotation on external imports, which esbuild will then preserve in the output for use in a browser (which would otherwise refuse to load certain imports).
With this release, esbuild now supports bundling code that uses import attributes (the second iteration). This is much more useful for bundlers because they are allowed to affect module resolution, which means the key-value pairs can be provided to plugins. Here's an example, which uses esbuild's built-in support for the upcoming JSON module standard:
// On static imports import foo from './package.json' with { type: 'json' } console.log(foo) // On dynamic imports const bar = await import('./package.json', { with: { type: 'json' } }) console.log(bar)One important consequence of the change in semantics between import assertions and import attributes is that two imports with identical paths but different import attributes are now considered to be different modules. This is because the import attributes are provided to the loader, which might then use those attributes during loading. For example, you could imagine an image loader that produces an image of a different size depending on the import attributes.
Import attributes are now reported in the metafile and are now provided to on-load plugins as a map in the
withproperty. For example, here's an esbuild plugin that turns all imports with atypeimport attribute equal to'cheese'into a module that exports the cheese emoji:const cheesePlugin = { name: 'cheese', setup(build) { build.onLoad({ filter: /.*/ }, args => { if (args.with.type === 'cheese') return { contents: `export default "🧀"`, } }) } } require('esbuild').build({ bundle: true, write: false, stdin: { contents: ` import foo from 'data:text/javascript,' with { type: 'cheese' } console.log(foo) `, }, plugins: [cheesePlugin], }).then(result => { const code = new Function(result.outputFiles[0].text) code() })Warning: It's possible that the second iteration of this feature may change significantly again even though it's already shipping in real JavaScript VMs (since it has already happened once before). In that case, esbuild may end up adjusting its implementation to match the eventual standard behavior. So keep in mind that by using this, you are using an unstable upcoming JavaScript feature that may undergo breaking changes in the future.
-
-
Adjust TypeScript experimental decorator behavior (#3230, #3326, #3394)
With this release, esbuild will now allow TypeScript experimental decorators to access both static class properties and
#privateclass names. For example:const check = <T,>(a: T, b: T): PropertyDecorator => () => console.log(a === b) async function test() { class Foo { static #foo = 1 static bar = 1 + Foo.#foo @check(Foo.#foo, 1) a: any @check(Foo.bar, await Promise.resolve(2)) b: any } } test().then(() => console.log('pass'))This will now print
true true passwhen compiled by esbuild. Previously esbuild evaluated TypeScript decorators outside of the class body, so it didn't allow decorators to accessFooor#foo. Now esbuild does something different, although it's hard to concisely explain exactly what esbuild is doing now (see the background section below for more information).Note that TypeScript's experimental decorator support is currently buggy: TypeScript's compiler passes this test if only the first
@checkis present or if only the second@checkis present, but TypeScript's compiler fails this test if both checks are present together. I haven't changed esbuild to match TypeScript's behavior exactly here because I'm waiting for TypeScript to fix these bugs instead.Some background: TypeScript experimental decorators don't have consistent semantics regarding the context that the decorators are evaluated in. For example, TypeScript will let you use
awaitwithin a decorator, which implies that the decorator runs outside the class body (sinceawaitisn't supported inside a class body), but TypeScript will also let you use#privatenames, which implies that the decorator runs inside the class body (since#privatenames are only supported inside a class body). The value ofthisin a decorator is also buggy (the run-time value ofthischanges if any decorator in the class uses a#privatename but the type ofthisdoesn't change, leading to the type checker no longer matching reality). These inconsistent semantics make it hard for esbuild to implement this feature as decorator evaluation happens in some superposition of both inside and outside the class body that is particular to the internal implementation details of the TypeScript compiler. -
Forbid
--keep-nameswhen targeting old browsers (#3477)The
--keep-namessetting needs to be able to assign to thenameproperty on functions and classes. However, before ES6 this property was non-configurable, and attempting to assign to it would throw an error. So with this release, esbuild will no longer allow you to enable this setting while also targeting a really old browser.
-
Fix a constant folding bug with bigint equality
This release fixes a bug where esbuild incorrectly checked for bigint equality by checking the equality of the bigint literal text. This is correct if the bigint doesn't have a radix because bigint literals without a radix are always in canonical form (since leading zeros are not allowed). However, this is incorrect if the bigint has a radix (e.g.
0x123n) because the canonical form is not enforced when a radix is present.// Original code console.log(!!0n, !!1n, 123n === 123n) console.log(!!0x0n, !!0x1n, 123n === 0x7Bn) // Old output console.log(false, true, true); console.log(true, true, false); // New output console.log(false, true, true); console.log(!!0x0n, !!0x1n, 123n === 0x7Bn); -
Add some improvements to the JavaScript minifier
This release adds more cases to the JavaScript minifier, including support for inlining
String.fromCharCodeandString.prototype.charCodeAtwhen possible:// Original code document.onkeydown = e => e.keyCode === 'A'.charCodeAt(0) && console.log(String.fromCharCode(55358, 56768)) // Old output (with --minify) document.onkeydown=o=>o.keyCode==="A".charCodeAt(0)&&console.log(String.fromCharCode(55358,56768)); // New output (with --minify) document.onkeydown=o=>o.keyCode===65&&console.log("🧀");In addition, immediately-invoked function expressions (IIFEs) that return a single expression are now inlined when minifying. This makes it possible to use IIFEs in combination with
@__PURE__annotations to annotate arbitrary expressions as side-effect free without the IIFE wrapper impacting code size. For example:// Original code const sideEffectFreeOffset = /* @__PURE__ */ (() => computeSomething())() use(sideEffectFreeOffset) // Old output (with --minify) const e=(()=>computeSomething())();use(e); // New output (with --minify) const e=computeSomething();use(e); -
Automatically prefix the
mask-compositeCSS property for WebKit (#3493)The
mask-compositeproperty will now be prefixed as-webkit-mask-compositefor older WebKit-based browsers. In addition to prefixing the property name, handling older browsers also requires rewriting the values since WebKit uses non-standard names for the mask composite modes:/* Original code */ div { mask-composite: add, subtract, intersect, exclude; } /* New output (with --target=chrome100) */ div { -webkit-mask-composite: source-over, source-out, source-in, xor; mask-composite: add, subtract, intersect, exclude; } -
Avoid referencing
thisfrom JSX elements in derived class constructors (#3454)When you enable
--jsx=automaticand--jsx-dev, the JSX transform is supposed to insertthisas the last argument to thejsxDEVfunction. I'm not sure exactly why this is and I can't find any specification for it, but in any case this causes the generated code to crash when you use a JSX element in a derived class constructor before the call tosuper()asthisis not allowed to be accessed at that point. For example// Original code class ChildComponent extends ParentComponent { constructor() { super(<div />) } } // Problematic output (with --loader=jsx --jsx=automatic --jsx-dev) import { jsxDEV } from "react/jsx-dev-runtime"; class ChildComponent extends ParentComponent { constructor() { super(/* @__PURE__ */ jsxDEV("div", {}, void 0, false, { fileName: "<stdin>", lineNumber: 3, columnNumber: 15 }, this)); // The reference to "this" crashes here } }The TypeScript compiler doesn't handle this at all while the Babel compiler just omits
thisfor the entire constructor (even after the call tosuper()). There seems to be no specification so I can't be sure that this change doesn't break anything important. But given that Babel is pretty loose with this and TypeScript doesn't handle this at all, I'm guessing this value isn't too important. React's blog post seems to indicate that this value was intended to be used for a React-specific migration warning at some point, so it could even be that this value is irrelevant now. Anyway the crash in this case should now be fixed. -
Allow package subpath imports to map to node built-ins (#3485)
You are now able to use a subpath import in your package to resolve to a node built-in module. For example, with a
package.jsonfile like this:{ "type": "module", "imports": { "#stream": { "node": "stream", "default": "./stub.js" } } }You can now import from node's
streammodule like this:import * as stream from '#stream'; console.log(Object.keys(stream));This will import from node's
streammodule when the platform isnodeand from./stub.jsotherwise. -
No longer throw an error when a
Symbolis missing (#3453)Certain JavaScript syntax features use special properties on the global
Symbolobject. For example, the asynchronous iteration syntax usesSymbol.asyncIterator. Previously esbuild's generated code for older browsers required this symbol to be polyfilled. However, starting with this release esbuild will useSymbol.for()to construct these symbols if they are missing instead of throwing an error about a missing polyfill. This means your code no longer needs to include a polyfill for missing symbols as long as your code also usesSymbol.for()for missing symbols. -
Parse upcoming changes to TypeScript syntax (#3490, #3491)
With this release, you can now use
fromas the name of a default type-only import in TypeScript code, as well asofas the name of anawait usingloop iteration variable:import type from from 'from' for (await using of of of) ;This matches similar changes in the TypeScript compiler (#56376 and #55555) which will start allowing this syntax in an upcoming version of TypeScript. Please never actually write code like this.
The type-only import syntax change was contributed by @magic-akari.
-
Fix a regression in 0.19.0 regarding
pathsintsconfig.json(#3354)The fix in esbuild version 0.19.0 to process
tsconfig.jsonaliases before the--packages=externalsetting unintentionally broke an edge case in esbuild's handling of certaintsconfig.jsonaliases where there are multiple files with the same name in different directories. This release adjusts esbuild's behavior for this edge case so that it passes while still processing aliases before--packages=external. Please read the linked issue for more details. -
Fix a CSS
fontproperty minification bug (#3452)This release fixes a bug where esbuild's CSS minifier didn't insert a space between the font size and the font family in the
fontCSS shorthand property in the edge case where the original source code didn't already have a space and the leading string token was shortened to an identifier:/* Original code */ .foo { font: 16px"Menlo"; } /* Old output (with --minify) */ .foo{font:16pxMenlo} /* New output (with --minify) */ .foo{font:16px Menlo} -
Fix bundling CSS with asset names containing spaces (#3410)
Assets referenced via CSS
url()tokens may cause esbuild to generate invalid output when bundling if the file name contains spaces (e.g.url(image 2.png)). With this release, esbuild will now quote all bundled asset references inurl()tokens to avoid this problem. This only affects assets loaded using thefileandcopyloaders. -
Fix invalid CSS
url()tokens in@importrules (#3426)In the future, CSS
url()tokens may contain additional stuff after the URL. This is irrelevant today as no CSS specification does this. But esbuild previously had a bug where using these tokens in an@importrule resulted in malformed output. This bug has been fixed. -
Fix
browser+false+type: moduleinpackage.json(#3367)The
browserfield inpackage.jsonallows you to map a file tofalseto have it be treated as an empty file when bundling for the browser. However, ifpackage.jsoncontains"type": "module"then all.jsfiles will be considered ESM, not CommonJS. Importing a named import from an empty CommonJS file gives you undefined, but importing a named export from an empty ESM file is a build error. This release changes esbuild's interpretation of these files mapped tofalsein this situation from ESM to CommonJS to avoid generating build errors for named imports. -
Fix a bug in top-level await error reporting (#3400)
Using
require()on a file that contains top-level await is not allowed becauserequire()must return synchronously and top-level await makes that impossible. You will get a build error if you try to bundle code that does this with esbuild. This release fixes a bug in esbuild's error reporting code for complex cases of this situation involving multiple levels of imports to get to the module containing the top-level await. -
Update to Unicode 15.1.0
The character tables that determine which characters form valid JavaScript identifiers have been updated from Unicode version 15.0.0 to the newly-released Unicode version 15.1.0. I'm not putting an example in the release notes because all of the new characters will likely just show up as little squares since fonts haven't been updated yet. But you can read https://www.unicode.org/versions/Unicode15.1.0/#Summary for more information about the changes.
This upgrade was contributed by @JLHwung.
-
Fix printing of JavaScript decorators in tricky cases (#3396)
This release fixes some bugs where esbuild's pretty-printing of JavaScript decorators could incorrectly produced code with a syntax error. The problem happened because esbuild sometimes substitutes identifiers for other expressions in the pretty-printer itself, but the decision about whether to wrap the expression or not didn't account for this. Here are some examples:
// Original code import { constant } from './constants.js' import { imported } from 'external' import { undef } from './empty.js' class Foo { @constant() @imported() @undef() foo } // Old output (with --bundle --format=cjs --packages=external --minify-syntax) var import_external = require("external"); var Foo = class { @123() @(0, import_external.imported)() @(void 0)() foo; }; // New output (with --bundle --format=cjs --packages=external --minify-syntax) var import_external = require("external"); var Foo = class { @(123()) @((0, import_external.imported)()) @((void 0)()) foo; }; -
Allow pre-release versions to be passed to
target(#3388)People want to be able to pass version numbers for unreleased versions of node (which have extra stuff after the version numbers) to esbuild's
targetsetting and have esbuild do something reasonable with them. These version strings are of course not present in esbuild's internal feature compatibility table because an unreleased version has not been released yet (by definition). With this release, esbuild will now attempt to accept these version strings passed totargetand do something reasonable with them.
-
Fix
list-style-typewith thelocal-cssloader (#3325)The
local-cssloader incorrectly treated all identifiers provided tolist-style-typeas a custom local identifier. That included identifiers such asnonewhich have special meaning in CSS, and which should not be treated as custom local identifiers. This release fixes this bug:/* Original code */ ul { list-style-type: none } /* Old output (with --loader=local-css) */ ul { list-style-type: stdin_none; } /* New output (with --loader=local-css) */ ul { list-style-type: none; }Note that this bug only affected code using the
local-cssloader. It did not affect code using thecssloader. -
Avoid inserting temporary variables before
use strict(#3322)This release fixes a bug where esbuild could incorrectly insert automatically-generated temporary variables before
use strictdirectives:// Original code function foo() { 'use strict' a.b?.c() } // Old output (with --target=es6) function foo() { var _a; "use strict"; (_a = a.b) == null ? void 0 : _a.c(); } // New output (with --target=es6) function foo() { "use strict"; var _a; (_a = a.b) == null ? void 0 : _a.c(); } -
Adjust TypeScript
enumoutput to better approximatetsc(#3329)TypeScript enum values can be either number literals or string literals. Numbers create a bidirectional mapping between the name and the value but strings only create a unidirectional mapping from the name to the value. When the enum value is neither a number literal nor a string literal, TypeScript and esbuild both default to treating it as a number:
// Original TypeScript code declare const foo: any enum Foo { NUMBER = 1, STRING = 'a', OTHER = foo, } // Compiled JavaScript code (from "tsc") var Foo; (function (Foo) { Foo[Foo["NUMBER"] = 1] = "NUMBER"; Foo["STRING"] = "a"; Foo[Foo["OTHER"] = foo] = "OTHER"; })(Foo || (Foo = {}));However, TypeScript does constant folding slightly differently than esbuild. For example, it may consider template literals to be string literals in some cases:
// Original TypeScript code declare const foo = 'foo' enum Foo { PRESENT = `${foo}`, MISSING = `${bar}`, } // Compiled JavaScript code (from "tsc") var Foo; (function (Foo) { Foo["PRESENT"] = "foo"; Foo[Foo["MISSING"] = `${bar}`] = "MISSING"; })(Foo || (Foo = {}));The template literal initializer for
PRESENTis treated as a string while the template literal initializer forMISSINGis treated as a number. Previously esbuild treated both of these cases as a number but starting with this release, esbuild will now treat both of these cases as a string. This doesn't exactly match the behavior oftscbut in the case where the behavior divergestscreports a compile error, so this seems like acceptible behavior for esbuild. Note that handling these cases completely correctly would require esbuild to parse type declarations (see thedeclarekeyword), which esbuild deliberately doesn't do. -
Ignore case in CSS in more places (#3316)
This release makes esbuild's CSS support more case-agnostic, which better matches how browsers work. For example:
/* Original code */ @KeyFrames Foo { From { OpaCity: 0 } To { OpaCity: 1 } } body { CoLoR: YeLLoW } /* Old output (with --minify) */ @KeyFrames Foo{From {OpaCity: 0} To {OpaCity: 1}}body{CoLoR:YeLLoW} /* New output (with --minify) */ @KeyFrames Foo{0%{OpaCity:0}To{OpaCity:1}}body{CoLoR:#ff0}Please never actually write code like this.
-
Improve the error message for
nullentries inexports(#3377)Package authors can disable package export paths with the
exportsmap inpackage.json. With this release, esbuild now has a clearer error message that points to thenulltoken inpackage.jsonitself instead of to the surrounding context. Here is an example of the new error message:✘ [ERROR] Could not resolve "msw/browser" lib/msw-config.ts:2:28: 2 │ import { setupWorker } from 'msw/browser'; ╵ ~~~~~~~~~~~~~ The path "./browser" cannot be imported from package "msw" because it was explicitly disabled by the package author here: node_modules/msw/package.json:17:14: 17 │ "node": null, ╵ ~~~~ You can mark the path "msw/browser" as external to exclude it from the bundle, which will remove this error and leave the unresolved path in the bundle. -
Parse and print the
withkeyword inimportstatementsJavaScript was going to have a feature called "import assertions" that adds an
assertkeyword toimportstatements. It looked like this:import stuff from './stuff.json' assert { type: 'json' }The feature provided a way to assert that the imported file is of a certain type (but was not allowed to affect how the import is interpreted, even though that's how everyone expected it to behave). The feature was fully specified and then actually implemented and shipped in Chrome before the people behind the feature realized that they should allow it to affect how the import is interpreted after all. So import assertions are no longer going to be added to the language.
Instead, the current proposal is to add a feature called "import attributes" instead that adds a
withkeyword to import statements. It looks like this:import stuff from './stuff.json' with { type: 'json' }This feature provides a way to affect how the import is interpreted. With this release, esbuild now has preliminary support for parsing and printing this new
withkeyword. Thewithkeyword is not yet interpreted by esbuild, however, so bundling code with it will generate a build error. All this release does is allow you to use esbuild to process code containing it (such as removing types from TypeScript code). Note that this syntax is not yet a part of JavaScript and may be removed or altered in the future if the specification changes (which it already has once, as described above). If that happens, esbuild reserves the right to remove or alter its support for this syntax too.
-
Update how CSS nesting is parsed again
CSS nesting syntax has been changed again, and esbuild has been updated to match. Type selectors may now be used with CSS nesting:
.foo { div { color: red; } }Previously this was disallowed in the CSS specification because it's ambiguous whether an identifier is a declaration or a nested rule starting with a type selector without requiring unbounded lookahead in the parser. It has now been allowed because the CSS working group has decided that requiring unbounded lookahead is acceptable after all.
Note that this change means esbuild no longer considers any existing browser to support CSS nesting since none of the existing browsers support this new syntax. CSS nesting will now always be transformed when targeting a browser. This situation will change in the future as browsers add support for this new syntax.
-
Fix a scope-related bug with
--drop-labels=(#3311)The recently-released
--drop-labels=feature previously had a bug where esbuild's internal scope stack wasn't being restored properly when a statement with a label was dropped. This could manifest as a tree-shaking issue, although it's possible that this could have also been causing other subtle problems too. The bug has been fixed in this release. -
Make renamed CSS names unique across entry points (#3295)
Previously esbuild's generated names for local names in CSS were only unique within a given entry point (or across all entry points when code splitting was enabled). That meant that building multiple entry points with esbuild could result in local names being renamed to the same identifier even when those entry points were built simultaneously within a single esbuild API call. This problem was especially likely to happen with minification enabled. With this release, esbuild will now avoid renaming local names from two separate entry points to the same name if those entry points were built with a single esbuild API call, even when code splitting is disabled.
-
Fix CSS ordering bug with
@layerbefore@importCSS lets you put
@layerrules before@importrules to define the order of layers in a stylesheet. Previously esbuild's CSS bundler incorrectly ordered these after the imported files because before the introduction of cascade layers to CSS, imported files could be bundled by removing the@importrules and then joining files together in the right order. But with@layer, CSS files may now need to be split apart into multiple pieces in the bundle. For example:/* Original code */ @layer start; @import "data:text/css,@layer inner.start;"; @import "data:text/css,@layer inner.end;"; @layer end; /* Old output (with --bundle) */ @layer inner.start; @layer inner.end; @layer start; @layer end; /* New output (with --bundle) */ @layer start; @layer inner.start; @layer inner.end; @layer end; -
Unwrap nested duplicate
@mediarules (#3226)With this release, esbuild's CSS minifier will now automatically unwrap duplicate nested
@mediarules:/* Original code */ @media (min-width: 1024px) { .foo { color: red } @media (min-width: 1024px) { .bar { color: blue } } } /* Old output (with --minify) */ @media (min-width: 1024px){.foo{color:red}@media (min-width: 1024px){.bar{color:#00f}}} /* New output (with --minify) */ @media (min-width: 1024px){.foo{color:red}.bar{color:#00f}}These rules are unlikely to be authored manually but may result from using frameworks such as Tailwind to generate CSS.
-
Fix a regression with
baseURLintsconfig.json(#3307)The previous release moved
tsconfig.jsonpath resolution before--packages=externalchecks to allow thepathsfield intsconfig.jsonto avoid a package being marked as external. However, that reordering accidentally broke the behavior of thebaseURLfield fromtsconfig.json. This release moves these path resolution rules around again in an attempt to allow both of these cases to work. -
Parse TypeScript type arguments for JavaScript decorators (#3308)
When parsing JavaScript decorators in TypeScript (i.e. with
experimentalDecoratorsdisabled), esbuild previously didn't parse type arguments. Type arguments will now be parsed starting with this release. For example:@foo<number> @bar<number, string>() class Foo {} -
Fix glob patterns matching extra stuff at the end (#3306)
Previously glob patterns such as
./*.jswould incorrectly behave like./*.js*during path matching (also matching.js.mapfiles, for example). This was never intentional behavior, and has now been fixed. -
Change the permissions of esbuild's generated output files (#3285)
This release changes the permissions of the output files that esbuild generates to align with the default behavior of node's
fs.writeFileSyncfunction. Since most tools written in JavaScript usefs.writeFileSync, this should make esbuild more consistent with how other JavaScript build tools behave.The full Unix-y details: Unix permissions use three-digit octal notation where the three digits mean "user, group, other" in that order. Within a digit, 4 means "read" and 2 means "write" and 1 means "execute". So 6 == 4 + 2 == read + write. Previously esbuild uses 0644 permissions (the leading 0 means octal notation) but the permissions for
fs.writeFileSyncdefaults to 0666, so esbuild will now use 0666 permissions. This does not necessarily mean that the files esbuild generates will end up having 0666 permissions, however, as there is another Unix feature called "umask" where the operating system masks out some of these bits. If your umask is set to 0022 then the generated files will have 0644 permissions, and if your umask is set to 0002 then the generated files will have 0664 permissions. -
Fix a subtle CSS ordering issue with
@importand@layerWith this release, esbuild may now introduce additional
@layerrules when bundling CSS to better preserve the layer ordering of the input code. Here's an example of an edge case where this matters:/* entry.css */ @import "a.css"; @import "b.css"; @import "a.css";/* a.css */ @layer a { body { background: red; } }/* b.css */ @layer b { body { background: green; } }This CSS should set the body background to
green, which is what happens in the browser. Previously esbuild generated the following output which incorrectly sets the body background tored:/* b.css */ @layer b { body { background: green; } } /* a.css */ @layer a { body { background: red; } }This difference in behavior is because the browser evaluates
a.css+b.css+a.css(in CSS, each@importis replaced with a copy of the imported file) while esbuild was only writing outb.css+a.css. The first copy ofa.csswasn't being written out by esbuild for two reasons: 1) bundlers care about code size and try to avoid emitting duplicate CSS and 2) when there are multiple copies of a CSS file, normally only the last copy matters since the last declaration with equal specificity wins in CSS.However,
@layerwas recently added to CSS and for@layerthe first copy matters because layers are ordered using their first location in source code order. This introduction of@layermeans esbuild needs to change its bundling algorithm. An easy solution would be for esbuild to write outa.csstwice, but that would be inefficient. So what I'm going to try to have esbuild do with this release is to write out an abbreviated form of the first copy of a CSS file that only includes the@layerinformation, and then still only write out the full CSS file once for the last copy. So esbuild's output for this edge case now looks like this:/* a.css */ @layer a; /* b.css */ @layer b { body { background: green; } } /* a.css */ @layer a { body { background: red; } }The behavior of the bundled CSS now matches the behavior of the unbundled CSS. You may be wondering why esbuild doesn't just write out
a.cssfirst followed byb.css. That would work in this case but it doesn't work in general because for any rules outside of a@layerrule, the last copy should still win instead of the first copy. -
Fix a bug with esbuild's TypeScript type definitions (#3299)
This release fixes a copy/paste error with the TypeScript type definitions for esbuild's JS API:
export interface TsconfigRaw { compilerOptions?: { - baseUrl?: boolean + baseUrl?: string ... } }This fix was contributed by @privatenumber.
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.18.0 or ~0.18.0. See npm's documentation about semver for more information.
-
Handle import paths containing wildcards (#56, #700, #875, #976, #2221, #2515)
This release introduces wildcards in import paths in two places:
-
Entry points
You can now pass a string containing glob-style wildcards such as
./src/*.tsas an entry point and esbuild will search the file system for files that match the pattern. This can be used to easily pass esbuild all files with a certain extension on the command line in a cross-platform way. Previously you had to rely on the shell to perform glob expansion, but that is obviously shell-dependent and didn't work at all on Windows. Note that to use this feature on the command line you will have to quote the pattern so it's passed verbatim to esbuild without any expansion by the shell. Here's an example:esbuild --minify "./src/*.ts" --outdir=outSpecifically the
*character will match any character except for the/character, and the/**/character sequence will match a path separator followed by zero or more path elements. Other wildcard operators found in glob patterns such as?and[...]are not supported. -
Run-time import paths
Import paths that are evaluated at run-time can now be bundled in certain limited situations. The import path expression must be a form of string concatenation and must start with either
./or../. Each non-string expression in the string concatenation chain becomes a wildcard. The*wildcard is chosen unless the previous character is a/, in which case the/**/*character sequence is used. Some examples:// These two forms are equivalent const json1 = await import('./data/' + kind + '.json') const json2 = await import(`./data/${kind}.json`)This feature works with
require(...)andimport(...)because these can all accept run-time expressions. It does not work withimportandexportstatements because these cannot accept run-time expressions. If you want to prevent esbuild from trying to bundle these imports, you should move the string concatenation expression outside of therequire(...)orimport(...). For example:// This will be bundled const json1 = await import('./data/' + kind + '.json') // This will not be bundled const path = './data/' + kind + '.json' const json2 = await import(path)Note that using this feature means esbuild will potentially do a lot of file system I/O to find all possible files that might match the pattern. This is by design, and is not a bug. If this is a concern, I recommend either avoiding the
/**/pattern (e.g. by not putting a/before a wildcard) or using this feature only in directory subtrees which do not have many files that don't match the pattern (e.g. making a subdirectory for your JSON files and explicitly including that subdirectory in the pattern).
-
-
Path aliases in
tsconfig.jsonno longer count as packages (#2792, #3003, #3160, #3238)Setting
--packages=externaltells esbuild to make all import paths external when they look like a package path. For example, an import of./foo/baris not a package path and won't be external while an import offoo/baris a package path and will be external. However, thepathsfield intsconfig.jsonallows you to create import paths that look like package paths but that do not resolve to packages. People do not want these paths to count as package paths. So with this release, the behavior of--packages=externalhas been changed to happen after thetsconfig.jsonpath remapping step. -
Use the
local-cssloader for.module.cssfiles by default (#20)With this release the
cssloader is still used for.cssfiles except that.module.cssfiles now use thelocal-cssloader. This is a common convention in the web development community. If you need.module.cssfiles to use thecssloader instead, then you can override this behavior with--loader:.module.css=css.
-
Support advanced CSS
@importrules (#953, #3137)CSS
@importstatements 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@mediarule. Here are some examples:@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. With this release, esbuild will now bundle
@importrules 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).
-
Implement
composesfrom CSS modules (#20)This release implements the
composesannotation from the CSS modules specification. It provides a way for class selectors to reference other class selectors (assuming you are using thelocal-cssloader). And with thefromsyntax, this can even work with local names across CSS files. For example:// app.js import { submit } from './style.css' const div = document.createElement('div') div.className = submit document.body.appendChild(div)/* style.css */ .button { composes: pulse from "anim.css"; display: inline-block; } .submit { composes: button; font-weight: bold; }/* anim.css */ @keyframes pulse { from, to { opacity: 1 } 50% { opacity: 0.5 } } .pulse { animation: 2s ease-in-out infinite pulse; }Bundling this with esbuild using
--bundle --outdir=dist --loader:.css=local-cssnow gives the following:(() => { // style.css var submit = "anim_pulse style_button style_submit"; // app.js var div = document.createElement("div"); div.className = submit; document.body.appendChild(div); })();/* anim.css */ @keyframes anim_pulse { from, to { opacity: 1; } 50% { opacity: 0.5; } } .anim_pulse { animation: 2s ease-in-out infinite anim_pulse; } /* style.css */ .style_button { display: inline-block; } .style_submit { font-weight: bold; }Import paths in the
composes: ... fromsyntax are resolved using the newcomposes-fromimport kind, which can be intercepted by plugins during import path resolution when bundling is enabled.Note that the order in which composed CSS classes from separate files appear in the bundled output file is deliberately undefined by design (see the specification for details). You are not supposed to declare the same CSS property in two separate class selectors and then compose them together. You are only supposed to compose CSS class selectors that declare non-overlapping CSS properties.
Issue #20 (the issue tracking CSS modules) is esbuild's most-upvoted issue! With this change, I now consider esbuild's implementation of CSS modules to be complete. There are still improvements to make and there may also be bugs with the current implementation, but these can be tracked in separate issues.
-
Fix non-determinism with
tsconfig.jsonand symlinks (#3284)This release fixes an issue that could cause esbuild to sometimes emit incorrect build output in cases where a file under the effect of
tsconfig.jsonis inconsistently referenced through a symlink. It can happen when usingnpm linkto create a symlink withinnode_modulesto an unpublished package. The build result was non-deterministic because esbuild runs module resolution in parallel and the result of thetsconfig.jsonlookup depended on whether the import through the symlink or not through the symlink was resolved first. This problem was fixed by moving therealpathoperation before thetsconfig.jsonlookup. -
Add a
hashproperty to output files (#3084, #3293)As a convenience, every output file in esbuild's API now includes a
hashproperty that is a hash of thecontentsfield. This is the hash that's used internally by esbuild to detect changes between builds for esbuild's live-reload feature. You may also use it to detect changes between your own builds if its properties are sufficient for your use case.This feature has been added directly to output file objects since it's just a hash of the
contentsfield, so it makes conceptual sense to store it in the same location. Another benefit of putting it there instead of including it as a part of the watch mode API is that it can be used without watch mode enabled. You can use it to compare the output of two independent builds that were done at different times.The hash algorithm (currently XXH64) is implementation-dependent and may be changed at any time in between esbuild versions. If you don't like esbuild's choice of hash algorithm then you are welcome to hash the contents yourself instead. As with any hash algorithm, note that while two different hashes mean that the contents are different, two equal hashes do not necessarily mean that the contents are equal. You may still want to compare the contents in addition to the hashes to detect with certainty when output files have been changed.
-
Avoid generating duplicate prefixed declarations in CSS (#3292)
There was a request for esbuild's CSS prefixer to avoid generating a prefixed declaration if a declaration by that name is already present in the same rule block. So with this release, esbuild will now avoid doing this:
/* Original code */ body { backdrop-filter: blur(30px); -webkit-backdrop-filter: blur(45px); } /* Old output (with --target=safari12) */ body { -webkit-backdrop-filter: blur(30px); backdrop-filter: blur(30px); -webkit-backdrop-filter: blur(45px); } /* New output (with --target=safari12) */ body { backdrop-filter: blur(30px); -webkit-backdrop-filter: blur(45px); }This can result in a visual difference in certain cases (for example if the browser understands
blur(30px)but notblur(45px), it will be able to fall back toblur(30px)). But this change means esbuild now matches the behavior of Autoprefixer which is probably a good representation of how people expect this feature to work.
-
Fix asset references with the
--line-limitflag (#3286)The recently-released
--line-limitflag tells esbuild to terminate long lines after they pass this length limit. This includes automatically wrapping long strings across multiple lines using escaped newline syntax. However, using this could cause esbuild to generate incorrect code for references from generated output files to assets in the bundle (i.e. files loaded with thefileorcopyloaders). This is because esbuild implements asset references internally using find-and-replace with a randomly-generated string, but the find operation fails if the string is split by an escaped newline due to line wrapping. This release fixes the problem by not wrapping these strings. This issue affected asset references in both JS and CSS files. -
Support local names in CSS for
@keyframe,@counter-style, and@container(#20)This release extends support for local names in CSS files loaded with the
local-cssloader to cover the@keyframe,@counter-style, and@containerrules (and alsoanimation,list-style, andcontainerdeclarations). Here's an example:@keyframes pulse { from, to { opacity: 1 } 50% { opacity: 0.5 } } @counter-style moon { system: cyclic; symbols: 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔; } @container squish { li { float: left } } ul { animation: 2s ease-in-out infinite pulse; list-style: inside moon; container: squish / size; }With the
local-cssloader enabled, that CSS will be turned into something like this (with the local name mapping exposed to JS):@keyframes stdin_pulse { from, to { opacity: 1; } 50% { opacity: 0.5; } } @counter-style stdin_moon { system: cyclic; symbols: 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔; } @container stdin_squish { li { float: left; } } ul { animation: 2s ease-in-out infinite stdin_pulse; list-style: inside stdin_moon; container: stdin_squish / size; }If you want to use a global name within a file loaded with the
local-cssloader, you can use a:globalselector to do that:div { /* All symbols are global inside this scope (i.e. * "pulse", "moon", and "squish" are global below) */ :global { animation: 2s ease-in-out infinite pulse; list-style: inside moon; container: squish / size; } }If you want to use
@keyframes,@counter-style, or@containerwith a global name, make sure it's in a file that uses thecssorglobal-cssloader instead of thelocal-cssloader. For example, you can configure--loader:.module.css=local-cssso that thelocal-cssloader only applies to*.module.cssfiles. -
Support strings as keyframe animation names in CSS (#2555)
With this release, esbuild will now parse animation names that are specified as strings and will convert them to identifiers. The CSS specification allows animation names to be specified using either identifiers or strings but Chrome only understands identifiers, so esbuild will now always convert string names to identifier names for Chrome compatibility:
/* Original code */ @keyframes "hide menu" { from { opacity: 1 } to { opacity: 0 } } menu.hide { animation: 0.5s ease-in-out "hide menu"; } /* Old output */ @keyframes "hide menu" { from { opacity: 1 } to { opacity: 0 } } menu.hide { animation: 0.5s ease-in-out "hide menu"; } /* New output */ @keyframes hide\ menu { from { opacity: 1; } to { opacity: 0; } } menu.hide { animation: 0.5s ease-in-out hide\ menu; }
-
Support
An+Bsyntax and:nth-*()pseudo-classes in CSSThis adds support for the
:nth-child(),:nth-last-child(),:nth-of-type(), and:nth-last-of-type()pseudo-classes to esbuild, which has the following consequences:- The
An+Bsyntax is now parsed, so parse errors are now reported An+Bvalues inside these pseudo-classes are now pretty-printed (e.g. a leading+will be stripped because it's not in the AST)- When minification is enabled,
An+Bvalues are reduced to equivalent but shorter forms (e.g.2n+0=>2n,2n+1=>odd) - Local CSS names in an
ofclause are now detected (e.g. in:nth-child(2n of :local(.foo))the namefoois now renamed)
/* Original code */ .foo:nth-child(+2n+1 of :local(.bar)) { color: red; } /* Old output (with --loader=local-css) */ .stdin_foo:nth-child(+2n + 1 of :local(.bar)) { color: red; } /* New output (with --loader=local-css) */ .stdin_foo:nth-child(2n+1 of .stdin_bar) { color: red; } - The
-
Adjust CSS nesting parser for IE7 hacks (#3272)
This fixes a regression with esbuild's treatment of IE7 hacks in CSS. CSS nesting allows selectors to be used where declarations are expected. There's an IE7 hack where prefixing a declaration with a
*causes that declaration to only be applied in IE7 due to a bug in IE7's CSS parser. However, it's valid for nested CSS selectors to start with*. So esbuild was incorrectly parsing these declarations and anything following it up until the next{as a selector for a nested CSS rule. This release changes esbuild's parser to terminate the parsing of selectors for nested CSS rules when a;is encountered to fix this edge case:/* Original code */ .item { *width: 100%; height: 1px; } /* Old output */ .item { *width: 100%; height: 1px; { } } /* New output */ .item { *width: 100%; height: 1px; }Note that the syntax for CSS nesting is about to change again, so esbuild's CSS parser may still not be completely accurate with how browsers do and/or will interpret CSS nesting syntax. Expect additional updates to esbuild's CSS parser in the future to deal with upcoming CSS specification changes.
-
Adjust esbuild's warning about undefined imports for TypeScript
importequals declarations (#3271)In JavaScript, accessing a missing property on an import namespace object is supposed to result in a value of
undefinedat run-time instead of an error at compile-time. This is something that esbuild warns you about by default because doing this can indicate a bug with your code. For example:// app.js import * as styles from './styles' console.log(styles.buton)// styles.js export let button = {}If you bundle
app.jswith esbuild you will get this:▲ [WARNING] Import "buton" will always be undefined because there is no matching export in "styles.js" [import-is-undefined] app.js:2:19: 2 │ console.log(styles.buton) │ ~~~~~ ╵ button Did you mean to import "button" instead? styles.js:1:11: 1 │ export let button = {} ╵ ~~~~~~However, there is TypeScript-only syntax for
importequals declarations that can represent either a type import (which esbuild should ignore) or a value import (which esbuild should respect). Since esbuild doesn't have a type system, it tries to only respectimportequals declarations that are actually used as values. Previously esbuild always generated this warning for unused imports referenced withinimportequals declarations even when the reference could be a type instead of a value. Starting with this release, esbuild will now only warn in this case if the import is actually used. Here is an example of some code that no longer causes an incorrect warning:// app.ts import * as styles from './styles' import ButtonType = styles.Button// styles.ts export interface Button {}
-
Fix a regression with whitespace inside
:is()(#3265)The change to parse the contents of
:is()in version 0.18.14 introduced a regression that incorrectly flagged the contents as a syntax error if the contents started with a whitespace token (for examplediv:is( .foo ) {}). This regression has been fixed.
-
Add the
--serve-fallback=option (#2904)The web server built into esbuild serves the latest in-memory results of the configured build. If the requested path doesn't match any in-memory build result, esbuild also provides the
--servedir=option to tell esbuild to serve the requested path from that directory instead. And if the requested path doesn't match either of those things, esbuild will either automatically generate a directory listing (for directories) or return a 404 error.Starting with this release, that last step can now be replaced with telling esbuild to serve a specific HTML file using the
--serve-fallback=option. This can be used to provide a "not found" page for missing URLs. It can also be used to implement a single-page app that mutates the current URL and therefore requires the single app entry point to be served when the page is loaded regardless of whatever the current URL is. -
Use the
tsconfigfield inpackage.jsonduringextendsresolution (#3247)This release adds a feature from TypeScript 3.2 where if a
tsconfig.jsonfile specifies a package name in theextendsfield and that package'spackage.jsonfile has atsconfigfield, the contents of that field are used in the search for the basetsconfig.jsonfile. -
Implement CSS nesting without
:is()when possible (#1945)Previously esbuild would always produce a warning when transforming nested CSS for a browser that doesn't support the
:is()pseudo-class. This was because the nesting transform needs to generate an:is()in some complex cases which means the transformed CSS would then not work in that browser. However, the CSS nesting transform can often be done without generating an:is(). So with this release, esbuild will no longer warn when targeting browsers that don't support:is()in the cases where an:is()isn't needed to represent the nested CSS.In addition, esbuild's nested CSS transform has been updated to avoid generating an
:is()in cases where an:is()is preferable but there's a longer alternative that is also equivalent. This update means esbuild can now generate a combinatorial explosion of CSS for complex CSS nesting syntax when targeting browsers that don't support:is(). This combinatorial explosion is necessary to accurately represent the original semantics. For example:/* Original code */ .first, .second, .third { & > & { color: red; } } /* Old output (with --target=chrome80) */ :is(.first, .second, .third) > :is(.first, .second, .third) { color: red; } /* New output (with --target=chrome80) */ .first > .first, .first > .second, .first > .third, .second > .first, .second > .second, .second > .third, .third > .first, .third > .second, .third > .third { color: red; }This change means you can now use CSS nesting with esbuild when targeting an older browser that doesn't support
:is(). You'll now only get a warning from esbuild if you use complex CSS nesting syntax that esbuild can't represent in that older browser without using:is(). There are two such cases:/* Case 1 */ a b { .foo & { color: red; } } /* Case 2 */ a { > b& { color: red; } }These two cases still need to use
:is(), both for different reasons, and cannot be used when targeting an older browser that doesn't support:is():/* Case 1 */ .foo :is(a b) { color: red; } /* Case 2 */ a > a:is(b) { color: red; } -
Automatically lower
insetin CSS for older browsersWith this release, esbuild will now automatically expand the
insetproperty to thetop,right,bottom, andleftproperties when esbuild'stargetis set to a browser that doesn't supportinset:/* Original code */ .app { position: absolute; inset: 10px 20px; } /* Old output (with --target=chrome80) */ .app { position: absolute; inset: 10px 20px; } /* New output (with --target=chrome80) */ .app { position: absolute; top: 10px; right: 20px; bottom: 10px; left: 20px; } -
Add support for the new
@starting-styleCSS rule (#3249)This at rule allow authors to start CSS transitions on first style update. That is, you can now make the transition take effect when the
displayproperty changes fromnonetoblock./* Original code */ @starting-style { h1 { background-color: transparent; } } /* Output */ @starting-style{h1{background-color:transparent}}This was contributed by @yisibl.
-
Implement local CSS names (#20)
This release introduces two new loaders called
global-cssandlocal-cssand two new pseudo-class selectors:local()and:global(). This is a partial implementation of the popular CSS modules approach for avoiding unintentional name collisions in CSS. I'm not calling this feature "CSS modules" because although some people in the community call it that, other people in the community have started using "CSS modules" to refer to something completely different and now CSS modules is an overloaded term.Here's how this new local CSS name feature works with esbuild:
-
Identifiers that look like
.classNameand#idNameare global with theglobal-cssloader and local with thelocal-cssloader. Global identifiers are the same across all files (the way CSS normally works) but local identifiers are different between different files. If two separate CSS files use the same local identifier.button, esbuild will automatically rename one of them so that they don't collide. This is analogous to how esbuild automatically renames JS local variables with the same name in separate JS files to avoid name collisions. -
It only makes sense to use local CSS names with esbuild when you are also using esbuild's bundler to bundle JS files that import CSS files. When you do that, esbuild will generate one export for each local name in the CSS file. The JS code can import these names and use them when constructing HTML DOM. For example:
// app.js import { outerShell } from './app.css' const div = document.createElement('div') div.className = outerShell document.body.appendChild(div)/* app.css */ .outerShell { position: absolute; inset: 0; }When you bundle this with
esbuild app.js --bundle --loader:.css=local-css --outdir=outyou'll now get this (notice how the local CSS nameouterShellhas been renamed):// out/app.js (() => { // app.css var outerShell = "app_outerShell"; // app.js var div = document.createElement("div"); div.className = outerShell; document.body.appendChild(div); })();/* out/app.css */ .app_outerShell { position: absolute; inset: 0; }This feature only makes sense to use when bundling is enabled both because your code needs to
importthe renamed local names so that it can use them, and because esbuild needs to be able to process all CSS files containing local names in a single bundling operation so that it can successfully rename conflicting local names to avoid collisions. -
If you are in a global CSS file (with the
global-cssloader) you can create a local name using:local(), and if you are in a local CSS file (with thelocal-cssloader) you can create a global name with:global(). So the choice of theglobal-cssloader vs. thelocal-cssloader just sets the default behavior for identifiers, but you can override it on a case-by-case basis as necessary. For example::local(.button) { color: red; } :global(.button) { color: blue; }Processing this CSS file with esbuild with either the
global-cssorlocal-cssloader will result in something like this:.stdin_button { color: red; } .button { color: blue; } -
The names that esbuild generates for local CSS names are an implementation detail and are not intended to be hard-coded anywhere. The only way you should be referencing the local CSS names in your JS or HTML is with an
importstatement in JS that is bundled with esbuild, as demonstrated above. For example, when--minifyis enabled esbuild will use a different name generation algorithm which generates names that are as short as possible (analogous to how esbuild minifies local identifiers in JS). -
You can easily use both global CSS files and local CSS files simultaneously if you give them different file extensions. For example, you could pass
--loader:.css=global-cssand--loader:.module.css=local-cssto esbuild so that.cssfiles still use global names by default but.module.cssfiles use local names by default. -
Keep in mind that the
cssloader is different than theglobal-cssloader. The:localand:globalannotations are not enabled with thecssloader and will be passed through unchanged. This allows you to have the option of using esbuild to process CSS containing while preserving these annotations. It also means that local CSS names are disabled by default for now (since thecssloader is currently the default for CSS files). The:localand:globalsyntax may be enabled by default in a future release.
Note that esbuild's implementation does not currently have feature parity with other implementations of modular CSS in similar tools. This is only a preliminary release with a partial implementation that includes some basic behavior to get the process started. Additional behavior may be added in future releases. In particular, this release does not implement:
- The
composespragma - Tree shaking for unused local CSS
- Local names for keyframe animations, grid lines,
@container,@counter-style, etc.
Issue #20 (the issue for this feature) is esbuild's most-upvoted issue! While this release still leaves that issue open, it's an important first step in that direction.
-
-
Parse
:is,:has,:not, and:wherein CSSWith this release, esbuild will now parse the contents of these pseudo-class selectors as a selector list. This means you will now get syntax warnings within these selectors for invalid selector syntax. It also means that esbuild's CSS nesting transform behaves slightly differently than before because esbuild is now operating on an AST instead of a token stream. For example:
/* Original code */ div { :where(.foo&) { color: red; } } /* Old output (with --target=chrome90) */ :where(.foo:is(div)) { color: red; } /* New output (with --target=chrome90) */ :where(div.foo) { color: red; }
-
Add the
--drop-labels=option (#2398)If you want to conditionally disable some development-only code and have it not be present in the final production bundle, right now the most straightforward way of doing this is to use the
--define:flag along with a specially-named global variable. For example, consider the following code:function main() { DEV && doAnExpensiveCheck() }You can build this for development and production like this:
- Development:
esbuild --define:DEV=true - Production:
esbuild --define:DEV=false
One drawback of this approach is that the resulting code crashes if you don't provide a value for
DEVwith--define:. In practice this isn't that big of a problem, and there are also various ways to work around this.However, another approach that avoids this drawback is to use JavaScript label statements instead. That's what the
--drop-labels=flag implements. For example, consider the following code:function main() { DEV: doAnExpensiveCheck() }With this release, you can now build this for development and production like this:
- Development:
esbuild - Production:
esbuild --drop-labels=DEV
This means that code containing optional development-only checks can now be written such that it's safe to run without any additional configuration. The
--drop-labels=flag takes comma-separated list of multiple label names to drop. - Development:
-
Avoid causing
unhandledRejectionduring shutdown (#3219)All pending esbuild JavaScript API calls are supposed to fail if esbuild's underlying child process is unexpectedly terminated. This can happen if
SIGINTis sent to the parentnodeprocess with Ctrl+C, for example. Previously doing this could also cause an unhandled promise rejection when esbuild attempted to communicate this failure to its own child process that no longer exists. This release now swallows this communication failure, which should prevent this internal unhandled promise rejection. This change means that you can now use esbuild's JavaScript API with a customSIGINThandler that extends the lifetime of thenodeprocess without esbuild's internals causing an early exit due to an unhandled promise rejection. -
Update browser compatibility table scripts
The scripts that esbuild uses to compile its internal browser compatibility table have been overhauled. Briefly:
- Converted from JavaScript to TypeScript
- Fixed some bugs that resulted in small changes to the table
- Added
caniuse-liteand@mdn/browser-compat-dataas new data sources (replacing manually-copied information)
This change means it's now much easier to keep esbuild's internal compatibility tables up to date. You can review the table changes here if you need to debug something about this change:
-
Fix a panic with
const enuminside parentheses (#3205)This release fixes an edge case where esbuild could potentially panic if a TypeScript
const enumstatement was used inside of a parenthesized expression and was followed by certain other scope-related statements. Here's a minimal example that triggers this edge case:(() => { const enum E { a }; () => E.a }) -
Allow a newline in the middle of TypeScript
export typestatement (#3225)Previously esbuild incorrectly rejected the following valid TypeScript code:
export type { T }; export type * as foo from 'bar';Code that uses a newline after
export typeis now allowed starting with this release. -
Fix cross-module inlining of string enums (#3210)
A refactoring typo in version 0.18.9 accidentally introduced a regression with cross-module inlining of string enums when combined with computed property accesses. This regression has been fixed.
-
Rewrite
.jsto.tsinside packages withexports(#3201)Packages with the
exportsfield are supposed to disable node's path resolution behavior that allows you to import a file with a different extension than the one in the source code (for example, importingfoo/barto getfoo/bar.js). And TypeScript has behavior where you can import a non-existent.jsfile and you will get the.tsfile instead. Previously the presence of theexportsfield caused esbuild to disable all extension manipulation stuff which included both node's implicit file extension searching and TypeScript's file extension swapping. However, TypeScript appears to always apply file extension swapping even in this case. So with this release, esbuild will now rewrite.jsto.tseven inside packages withexports. -
Fix a redirect edge case in esbuild's development server (#3208)
The development server canonicalizes directory URLs by adding a trailing slash. For example, visiting
/aboutredirects to/about/if/about/index.htmlwould be served. However, if the requested path begins with two slashes, then the redirect incorrectly turned into a protocol-relative URL. For example, visiting//aboutredirected to//about/which the browser turns intohttp://about/. This release fixes the bug by canonicalizing the URL path when doing this redirect.
-
Fix a TypeScript code generation edge case (#3199)
This release fixes a regression in version 0.18.4 where using a TypeScript
namespacethat exports aclassdeclaration combined with--keep-namesand a--targetofes2021or earlier could cause esbuild to export the class from the namespace using an incorrect name (notice the assignment toX2._Yvs.X2.Y):// Original code // Old output (with --keep-names --target=es2021) var X; ((X2) => { const _Y = class _Y { }; __name(_Y, "Y"); let Y = _Y; X2._Y = _Y; })(X || (X = {})); // New output (with --keep-names --target=es2021) var X; ((X2) => { const _Y = class _Y { }; __name(_Y, "Y"); let Y = _Y; X2.Y = _Y; })(X || (X = {}));
-
Fix a tree-shaking bug that removed side effects (#3195)
This fixes a regression in version 0.18.4 where combining
--minify-syntaxwith--keep-namescould cause expressions with side effects after a function declaration to be considered side-effect free for tree shaking purposes. The reason was because--keep-namesgenerates an expression statement containing a call to a helper function after the function declaration with a special flag that makes the function call able to be tree shaken, and then--minify-syntaxcould potentially merge that expression statement with following expressions without clearing the flag. This release fixes the bug by clearing the flag when merging expression statements together. -
Fix an incorrect warning about CSS nesting (#3197)
A warning is currently generated when transforming nested CSS to a browser that doesn't support
:is()because transformed nested CSS may need to use that feature to represent nesting. This was previously always triggered when an at-rule was encountered in a declaration context. Typically the only case you would encounter this is when using CSS nesting within a selector rule. However, there is a case where that's not true: when using a margin at-rule such as@top-leftwithin@page. This release avoids incorrectly generating a warning in this case by checking that the at-rule is within a selector rule before generating a warning.
-
Fix
await usingdeclarations insideasyncgenerator functionsI forgot about the new
await usingdeclarations when implementing lowering forasyncgenerator functions in the previous release. This change fixes the transformation ofawait usingdeclarations when they are inside loweredasyncgenerator functions:// Original code async function* foo() { await using x = await y } // Old output (with --supported:async-generator=false) function foo() { return __asyncGenerator(this, null, function* () { await using x = yield new __await(y); }); } // New output (with --supported:async-generator=false) function foo() { return __asyncGenerator(this, null, function* () { var _stack = []; try { const x = __using(_stack, yield new __await(y), true); } catch (_) { var _error = _, _hasError = true; } finally { var _promise = __callDispose(_stack, _error, _hasError); _promise && (yield new __await(_promise)); } }); } -
Insert some prefixed CSS properties when appropriate (#3122)
With this release, esbuild will now insert prefixed CSS properties in certain cases when the
targetsetting includes browsers that require a certain prefix. This is currently done for the following properties:appearance: *;=>-webkit-appearance: *; -moz-appearance: *;backdrop-filter: *;=>-webkit-backdrop-filter: *;background-clip: text=>-webkit-background-clip: text;box-decoration-break: *;=>-webkit-box-decoration-break: *;clip-path: *;=>-webkit-clip-path: *;font-kerning: *;=>-webkit-font-kerning: *;hyphens: *;=>-webkit-hyphens: *;initial-letter: *;=>-webkit-initial-letter: *;mask-image: *;=>-webkit-mask-image: *;mask-origin: *;=>-webkit-mask-origin: *;mask-position: *;=>-webkit-mask-position: *;mask-repeat: *;=>-webkit-mask-repeat: *;mask-size: *;=>-webkit-mask-size: *;position: sticky;=>position: -webkit-sticky;print-color-adjust: *;=>-webkit-print-color-adjust: *;tab-size: *;=>-moz-tab-size: *; -o-tab-size: *;text-decoration-color: *;=>-webkit-text-decoration-color: *; -moz-text-decoration-color: *;text-decoration-line: *;=>-webkit-text-decoration-line: *; -moz-text-decoration-line: *;text-decoration-skip: *;=>-webkit-text-decoration-skip: *;text-emphasis-color: *;=>-webkit-text-emphasis-color: *;text-emphasis-position: *;=>-webkit-text-emphasis-position: *;text-emphasis-style: *;=>-webkit-text-emphasis-style: *;text-orientation: *;=>-webkit-text-orientation: *;text-size-adjust: *;=>-webkit-text-size-adjust: *; -ms-text-size-adjust: *;user-select: *;=>-webkit-user-select: *; -moz-user-select: *; -ms-user-select: *;
Here is an example:
/* Original code */ div { mask-image: url(x.png); } /* Old output (with --target=chrome99) */ div { mask-image: url(x.png); } /* New output (with --target=chrome99) */ div { -webkit-mask-image: url(x.png); mask-image: url(x.png); }Browser compatibility data was sourced from the tables on https://caniuse.com. Support for more CSS properties can be added in the future as appropriate.
-
Fix an obscure identifier minification bug (#2809)
Function declarations in nested scopes behave differently depending on whether or not
"use strict"is present. To avoid generating code that behaves differently depending on whether strict mode is enabled or not, esbuild transforms nested function declarations into variable declarations. However, there was a bug where the generated variable name was not being recorded as declared internally, which meant that it wasn't being renamed correctly by the minifier and could cause a name collision. This bug has been fixed:// Original code const n = '' for (let i of [0,1]) { function f () {} } // Old output (with --minify-identifiers --format=esm) const f = ""; for (let o of [0, 1]) { let n = function() { }; var f = n; } // New output (with --minify-identifiers --format=esm) const f = ""; for (let o of [0, 1]) { let n = function() { }; var t = n; } -
Fix a bug in esbuild's compatibility table script (#3179)
Setting esbuild's
targetto a specific JavaScript engine tells esbuild to use the JavaScript syntax feature compatibility data from https://kangax.github.io/compat-table/es6/ for that engine to determine which syntax features to allow. However, esbuild's script that builds this internal compatibility table had a bug that incorrectly ignores tests for engines that still have outstanding implementation bugs which were never fixed. This change fixes this bug with the script.The only case where this changed the information in esbuild's internal compatibility table is that the
hermestarget is marked as no longer supporting destructuring. This is because there is a failing destructuring-related test for Hermes on https://kangax.github.io/compat-table/es6/. If you want to use destructuring with Hermes anyway, you can pass--supported:destructuring=trueto esbuild to override thehermestarget and force esbuild to accept this syntax.This fix was contributed by @ArrayZoneYour.
-
Implement transforming
asyncgenerator functions (#2780)With this release, esbuild will now transform
asyncgenerator functions into normal generator functions when the configured target environment doesn't support them. These functions behave similar to normal generator functions except that they use theSymbol.asyncIteratorinterface instead of theSymbol.iteratorinterface and the iteration methods return promises. Here's an example (helper functions are omitted):// Original code async function* foo() { yield Promise.resolve(1) await new Promise(r => setTimeout(r, 100)) yield *[Promise.resolve(2)] } async function bar() { for await (const x of foo()) { console.log(x) } } bar() // New output (with --target=es6) function foo() { return __asyncGenerator(this, null, function* () { yield Promise.resolve(1); yield new __await(new Promise((r) => setTimeout(r, 100))); yield* __yieldStar([Promise.resolve(2)]); }); } function bar() { return __async(this, null, function* () { try { for (var iter = __forAwait(foo()), more, temp, error; more = !(temp = yield iter.next()).done; more = false) { const x = temp.value; console.log(x); } } catch (temp) { error = [temp]; } finally { try { more && (temp = iter.return) && (yield temp.call(iter)); } finally { if (error) throw error[0]; } } }); } bar();This is an older feature that was added to JavaScript in ES2018 but I didn't implement the transformation then because it's a rarely-used feature. Note that esbuild already added support for transforming
for awaitloops (the other part of the asynchronous iteration proposal) a year ago, so support for asynchronous iteration should now be complete.I have never used this feature myself and code that uses this feature is hard to come by, so this transformation has not yet been tested on real-world code. If you do write code that uses this feature, please let me know if esbuild's
asyncgenerator transformation doesn't work with your code.
-
Add support for
usingdeclarations in TypeScript 5.2+ (#3191)TypeScript 5.2 (due to be released in August of 2023) will introduce
usingdeclarations, which will allow you to automatically dispose of the declared resources when leaving the current scope. You can read the TypeScript PR for this feature for more information. This release of esbuild adds support for transforming this syntax to target environments without support forusingdeclarations (which is currently all targets other thanesnext). Here's an example (helper functions are omitted):// Original code class Foo { [Symbol.dispose]() { console.log('cleanup') } } using foo = new Foo; foo.bar(); // New output (with --target=es6) var _stack = []; try { var Foo = class { [Symbol.dispose]() { console.log("cleanup"); } }; var foo = __using(_stack, new Foo()); foo.bar(); } catch (_) { var _error = _, _hasError = true; } finally { __callDispose(_stack, _error, _hasError); }The injected helper functions ensure that the method named
Symbol.disposeis called onnew Foowhen control exits the scope. Note that as with all new JavaScript APIs, you'll need to polyfillSymbol.disposeif it's not present before you use it. This is not something that esbuild does for you because esbuild only handles syntax, not APIs. Polyfilling it can be done with something like this:Symbol.dispose ||= Symbol('Symbol.dispose')This feature also introduces
await usingdeclarations which are likeusingdeclarations but they callawaiton the disposal method (not on the initializer). Here's an example (helper functions are omitted):// Original code class Foo { async [Symbol.asyncDispose]() { await new Promise(done => { setTimeout(done, 1000) }) console.log('cleanup') } } await using foo = new Foo; foo.bar(); // New output (with --target=es2022) var _stack = []; try { var Foo = class { async [Symbol.asyncDispose]() { await new Promise((done) => { setTimeout(done, 1e3); }); console.log("cleanup"); } }; var foo = __using(_stack, new Foo(), true); foo.bar(); } catch (_) { var _error = _, _hasError = true; } finally { var _promise = __callDispose(_stack, _error, _hasError); _promise && await _promise; }The injected helper functions ensure that the method named
Symbol.asyncDisposeis called onnew Foowhen control exits the scope, and that the returned promise is awaited. Similarly toSymbol.dispose, you'll also need to polyfillSymbol.asyncDisposebefore you use it. -
Add a
--line-limit=flag to limit line length (#3170)Long lines are common in minified code. However, many tools and text editors can't handle long lines. This release introduces the
--line-limit=flag to tell esbuild to wrap lines longer than the provided number of bytes. For example,--line-limit=80tells esbuild to insert a newline soon after a given line reaches 80 bytes in length. This setting applies to both JavaScript and CSS, and works even when minification is disabled. Note that turning this setting on will make your files bigger, as the extra newlines take up additional space in the file (even after gzip compression).
-
Fix tree-shaking of classes with decorators (#3164)
This release fixes a bug where esbuild incorrectly allowed tree-shaking on classes with decorators. Each decorator is a function call, so classes with decorators must never be tree-shaken. This bug was a regression that was unintentionally introduced in version 0.18.2 by the change that enabled tree-shaking of lowered private fields. Previously decorators were always lowered, and esbuild always considered the automatically-generated decorator code to be a side effect. But this is no longer the case now that esbuild analyzes side effects using the AST before lowering takes place. This bug was fixed by considering any decorator a side effect.
-
Fix a minification bug involving function expressions (#3125)
When minification is enabled, esbuild does limited inlining of
constsymbols at the top of a scope. This release fixes a bug where inlineable symbols were incorrectly removed assuming that they were inlined. They may not be inlined in cases where they were referenced by earlier constants in the body of a function expression. The declarations involved in these edge cases are now kept instead of being removed:// Original code { const fn = () => foo const foo = 123 console.log(fn) } // Old output (with --minify-syntax) console.log((() => foo)()); // New output (with --minify-syntax) { const fn = () => foo, foo = 123; console.log(fn); }
-
Implement auto accessors (#3009)
This release implements the new auto-accessor syntax from the upcoming JavaScript decorators proposal. The auto-accessor syntax looks like this:
class Foo { accessor foo; static accessor bar; } new Foo().foo = Foo.bar;This syntax is not yet a part of JavaScript but it was added to TypeScript in version 4.9. More information about this feature can be found in microsoft/TypeScript#49705. Auto-accessors will be transformed if the target is set to something other than
esnext:// Output (with --target=esnext) class Foo { accessor foo; static accessor bar; } new Foo().foo = Foo.bar; // Output (with --target=es2022) class Foo { #foo; get foo() { return this.#foo; } set foo(_) { this.#foo = _; } static #bar; static get bar() { return this.#bar; } static set bar(_) { this.#bar = _; } } new Foo().foo = Foo.bar; // Output (with --target=es2021) var _foo, _bar; class Foo { constructor() { __privateAdd(this, _foo, void 0); } get foo() { return __privateGet(this, _foo); } set foo(_) { __privateSet(this, _foo, _); } static get bar() { return __privateGet(this, _bar); } static set bar(_) { __privateSet(this, _bar, _); } } _foo = new WeakMap(); _bar = new WeakMap(); __privateAdd(Foo, _bar, void 0); new Foo().foo = Foo.bar;You can also now use auto-accessors with esbuild's TypeScript experimental decorator transformation, which should behave the same as decorating the underlying getter/setter pair.
Please keep in mind that this syntax is not yet part of JavaScript. This release enables auto-accessors in
.jsfiles with the expectation that it will be a part of JavaScript soon. However, esbuild may change or remove this feature in the future if JavaScript ends up changing or removing this feature. Use this feature with caution for now. -
Pass through JavaScript decorators (#104)
In this release, esbuild now parses decorators from the upcoming JavaScript decorators proposal and passes them through to the output unmodified (as long as the language target is set to
esnext). Transforming JavaScript decorators to environments that don't support them has not been implemented yet. The only decorator transform that esbuild currently implements is still the TypeScript experimental decorator transform, which only works in.tsfiles and which requires"experimentalDecorators": truein yourtsconfig.jsonfile. -
Static fields with assign semantics now use static blocks if possible
Setting
useDefineForClassFieldsto false in TypeScript requires rewriting class fields to assignment statements. Previously this was done by removing the field from the class body and adding an assignment statement after the class declaration. However, this also caused any private fields to also be lowered by necessity (in case a field initializer uses a private symbol, either directly or indirectly). This release changes this transform to use an inline static block if it's supported, which avoids needing to lower private fields in this scenario:// Original code class Test { static #foo = 123 static bar = this.#foo } // Old output (with useDefineForClassFields=false) var _foo; const _Test = class _Test { }; _foo = new WeakMap(); __privateAdd(_Test, _foo, 123); _Test.bar = __privateGet(_Test, _foo); let Test = _Test; // New output (with useDefineForClassFields=false) class Test { static #foo = 123; static { this.bar = this.#foo; } } -
Fix TypeScript experimental decorators combined with
--mangle-props(#3177)Previously using TypeScript experimental decorators combined with the
--mangle-propssetting could result in a crash, as the experimental decorator transform was not expecting a mangled property as a class member. This release fixes the crash so you can now combine both of these features together safely.
-
Bundling no longer unnecessarily transforms class syntax (#1360, #1328, #1524, #2416)
When bundling, esbuild automatically converts top-level class statements to class expressions. Previously this conversion had the unfortunate side-effect of also transforming certain other class-related syntax features to avoid correctness issues when the references to the class name within the class body. This conversion has been reworked to avoid doing this:
// Original code export class Foo { static foo = () => Foo } // Old output (with --bundle) var _Foo = class { }; var Foo = _Foo; __publicField(Foo, "foo", () => _Foo); // New output (with --bundle) var Foo = class _Foo { static foo = () => _Foo; };This conversion process is very complicated and has many edge cases (including interactions with static fields, static blocks, private class properties, and TypeScript experimental decorators). It should already be pretty robust but a change like this may introduce new unintentional behavior. Please report any issues with this upgrade on the esbuild bug tracker.
You may be wondering why esbuild needs to do this at all. One reason to do this is that esbuild's bundler sometimes needs to lazily-evaluate a module. For example, a module may end up being both the target of a dynamic
import()call and a staticimportstatement. Lazy module evaluation is done by wrapping the top-level module code in a closure. To avoid a performance hit for staticimportstatements, esbuild stores top-level exported symbols outside of the closure and references them directly instead of indirectly.Another reason to do this is that multiple JavaScript VMs have had and continue to have performance issues with TDZ (i.e. "temporal dead zone") checks. These checks validate that a let, or const, or class symbol isn't used before it's initialized. Here are two issues with well-known VMs:
- V8: https://bugs.chromium.org/p/v8/issues/detail?id=13723 (10% slowdown)
- JavaScriptCore: https://bugs.webkit.org/show_bug.cgi?id=199866 (1,000% slowdown!)
JavaScriptCore had a severe performance issue as their TDZ implementation had time complexity that was quadratic in the number of variables needing TDZ checks in the same scope (with the top-level scope typically being the worst offender). V8 has ongoing issues with TDZ checks being present throughout the code their JIT generates even when they have already been checked earlier in the same function or when the function in question has already been run (so the checks have already happened).
Due to esbuild's parallel architecture, esbuild both a) needs to convert class statements into class expressions during parsing and b) doesn't yet know whether this module will need to be lazily-evaluated or not in the parser. So esbuild always does this conversion during bundling in case it's needed for correctness (and also to avoid potentially catastrophic performance issues due to bundling creating a large scope with many TDZ variables).
-
Enforce TDZ errors in computed class property keys (#2045)
JavaScript allows class property keys to be generated at run-time using code, like this:
class Foo { static foo = 'foo' static [Foo.foo + '2'] = 2 }Previously esbuild treated references to the containing class name within computed property keys as a reference to the partially-initialized class object. That meant code that attempted to reference properties of the class object (such as the code above) would get back
undefinedinstead of throwing an error.This release rewrites references to the containing class name within computed property keys into code that always throws an error at run-time, which is how this JavaScript code is supposed to work. Code that does this will now also generate a warning. You should never write code like this, but it now should be more obvious when incorrect code like this is written.
-
Fix an issue with experimental decorators and static fields (#2629)
This release also fixes a bug regarding TypeScript experimental decorators and static class fields which reference the enclosing class name in their initializer. This affected top-level classes when bundling was enabled. Previously code that does this could crash because the class name wasn't initialized yet. This case should now be handled correctly:
// Original code class Foo { @someDecorator static foo = 'foo' static bar = Foo.foo.length } // Old output const _Foo = class { static foo = "foo"; static bar = _Foo.foo.length; }; let Foo = _Foo; __decorateClass([ someDecorator ], Foo, "foo", 2); // New output const _Foo = class _Foo { static foo = "foo"; static bar = _Foo.foo.length; }; __decorateClass([ someDecorator ], _Foo, "foo", 2); let Foo = _Foo; -
Fix a minification regression with negative numeric properties (#3169)
Version 0.18.0 introduced a regression where computed properties with negative numbers were incorrectly shortened into a non-computed property when minification was enabled. This regression has been fixed:
// Original code x = { [1]: 1, [-1]: -1, [NaN]: NaN, [Infinity]: Infinity, [-Infinity]: -Infinity, } // Old output (with --minify) x={1:1,-1:-1,NaN:NaN,1/0:1/0,-1/0:-1/0}; // New output (with --minify) x={1:1,[-1]:-1,NaN:NaN,[1/0]:1/0,[-1/0]:-1/0};
-
Fix a panic due to empty static class blocks (#3161)
This release fixes a bug where an internal invariant that was introduced in the previous release was sometimes violated, which then caused a panic. It happened when bundling code containing an empty static class block with both minification and bundling enabled.
-
Lower static blocks when static fields are lowered (#2800, #2950, #3025)
This release fixes a bug where esbuild incorrectly did not lower static class blocks when static class fields needed to be lowered. For example, the following code should print
1 2 3but previously printed2 1 3instead due to this bug:// Original code class Foo { static x = console.log(1) static { console.log(2) } static y = console.log(3) } // Old output (with --supported:class-static-field=false) class Foo { static { console.log(2); } } __publicField(Foo, "x", console.log(1)); __publicField(Foo, "y", console.log(3)); // New output (with --supported:class-static-field=false) class Foo { } __publicField(Foo, "x", console.log(1)); console.log(2); __publicField(Foo, "y", console.log(3)); -
Use static blocks to implement
--keep-nameson classes (#2389)This change fixes a bug where the
nameproperty could previously be incorrect within a class static context when using--keep-names. The problem was that thenameproperty was being initialized after static blocks were run instead of before. This has been fixed by moving thenameproperty initializer into a static block at the top of the class body:// Original code if (typeof Foo === 'undefined') { let Foo = class { static test = this.name } console.log(Foo.test) } // Old output (with --keep-names) if (typeof Foo === "undefined") { let Foo2 = /* @__PURE__ */ __name(class { static test = this.name; }, "Foo"); console.log(Foo2.test); } // New output (with --keep-names) if (typeof Foo === "undefined") { let Foo2 = class { static { __name(this, "Foo"); } static test = this.name; }; console.log(Foo2.test); }This change was somewhat involved, especially regarding what esbuild considers to be side-effect free. Some unused classes that weren't removed by tree shaking in previous versions of esbuild may now be tree-shaken. One example is classes with static private fields that are transformed by esbuild into code that doesn't use JavaScript's private field syntax. Previously esbuild's tree shaking analysis ran on the class after syntax lowering, but with this release it will run on the class before syntax lowering, meaning it should no longer be confused by class mutations resulting from automatically-generated syntax lowering code.
-
Fill in
nullentries in input source maps (#3144)If esbuild bundles input files with source maps and those source maps contain a
sourcesContentarray withnullentries, esbuild previously copied thosenullentries over to the output source map. With this release, esbuild will now attempt to fill in thosenullentries by looking for a file on the file system with the corresponding name from thesourcesarray. This matches esbuild's existing behavior that automatically generates thesourcesContentarray from the file system if the entiresourcesContentarray is missing. -
Support
/* @__KEY__ */comments for mangling property names (#2574)Property mangling is an advanced feature that enables esbuild to minify certain property names, even though it's not possible to automatically determine that it's safe to do so. The safe property names are configured via regular expression such as
--mangle-props=_$(mangle all properties ending in_).Sometimes it's desirable to also minify strings containing property names, even though it's not possible to automatically determine which strings are property names. This release makes it possible to do this by annotating those strings with
/* @__KEY__ */. This is a convention that Terser added earlier this year, and which esbuild is now following too: https://github.com/terser/terser/pull/1365. Using it looks like this:// Original code console.log( [obj.mangle_, obj.keep], [obj.get('mangle_'), obj.get('keep')], [obj.get(/* @__KEY__ */ 'mangle_'), obj.get(/* @__KEY__ */ 'keep')], ) // Old output (with --mangle-props=_$) console.log( [obj.a, obj.keep], [obj.get("mangle_"), obj.get("keep")], [obj.get(/* @__KEY__ */ "mangle_"), obj.get(/* @__KEY__ */ "keep")] ); // New output (with --mangle-props=_$) console.log( [obj.a, obj.keep], [obj.get("mangle_"), obj.get("keep")], [obj.get(/* @__KEY__ */ "a"), obj.get(/* @__KEY__ */ "keep")] ); -
Support
/* @__NO_SIDE_EFFECTS__ */comments for functions (#3149)Rollup has recently added support for
/* @__NO_SIDE_EFFECTS__ */annotations before functions to indicate that calls to these functions can be removed if the result is unused (i.e. the calls can be assumed to have no side effects). This release adds basic support for these to esbuild as well, which means esbuild will now parse these comments in input files and preserve them in output files. This should help people that use esbuild in combination with Rollup.Note that this doesn't necessarily mean esbuild will treat these calls as having no side effects, as esbuild's parallel architecture currently isn't set up to enable this type of cross-file tree-shaking information (tree-shaking decisions regarding a function call are currently local to the file they appear in). If you want esbuild to consider a function call to have no side effects, make sure you continue to annotate the function call with
/* @__PURE__ */(which is the previously-established convention for communicating this).
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.17.0 or ~0.17.0. See npm's documentation about semver for more information.
The breaking changes in this release mainly focus on fixing some long-standing issues with esbuild's handling of tsconfig.json files. Here are all the changes in this release, in detail:
-
Add a way to try esbuild online (#797)
There is now a way to try esbuild live on esbuild's website without installing it: https://esbuild.github.io/try/. In addition to being able to more easily evaluate esbuild, this should also make it more efficient to generate esbuild bug reports. For example, you can use it to compare the behavior of different versions of esbuild on the same input. The state of the page is stored in the URL for easy sharing. Many thanks to @hyrious for creating https://hyrious.me/esbuild-repl/, which was the main inspiration for this addition to esbuild's website.
Two forms of build options are supported: either CLI-style (example) or JS-style (example). Both are converted into a JS object that's passed to esbuild's WebAssembly API. The CLI-style argument parser is a custom one that simulates shell quoting rules, and the JS-style argument parser is also custom and parses a superset of JSON (basically JSON5 + regular expressions). So argument parsing is an approximate simulation of what happens for real but hopefully it should be close enough.
-
Changes to esbuild's
tsconfig.jsonsupport (#3019):This release makes the following changes to esbuild's
tsconfig.jsonsupport:-
Using experimental decorators now requires
"experimentalDecorators": true(#104)Previously esbuild would always compile decorators in TypeScript code using TypeScript's experimental decorator transform. Now that standard JavaScript decorators are close to being finalized, esbuild will now require you to use
"experimentalDecorators": trueto do this. This new requirement makes it possible for esbuild to introduce a transform for standard JavaScript decorators in TypeScript code in the future. Such a transform has not been implemented yet, however. -
TypeScript's
targetno longer affects esbuild'starget(#2628)Some people requested that esbuild support TypeScript's
targetsetting, so support for it was added (in version 0.12.4). However, esbuild supports reading from multipletsconfig.jsonfiles within a single build, which opens up the possibility that different files in the build have different language targets configured. There isn't really any reason to do this and it can lead to unexpected results. So with this release, thetargetsetting intsconfig.jsonwill no longer affect esbuild's owntargetsetting. You will have to use esbuild's own target setting instead (which is a single, global value). -
TypeScript's
jsxsetting no longer causes esbuild to preserve JSX syntax (#2634)TypeScript has a setting called
jsxthat controls how to transform JSX into JS. The tool-agnostic transform is calledreact, and the React-specific transform is calledreact-jsx(orreact-jsxdev). There is also a setting calledpreservewhich indicates JSX should be passed through untransformed. Previously people would run esbuild with"jsx": "preserve"in theirtsconfig.jsonfiles and then be surprised when esbuild preserved their JSX. So with this release, esbuild will now ignore"jsx": "preserve"intsconfig.jsonfiles. If you want to preserve JSX syntax with esbuild, you now have to use--jsx=preserve.Note: Some people have suggested that esbuild's equivalent
jsxsetting override the one intsconfig.json. However, some projects need to legitimately have different files within the same build use different transforms (i.e.reactvs.react-jsx) and having esbuild's globaljsxsetting overridetsconfig.jsonwould prevent this from working. This release ignores"jsx": "preserve"but still allows otherjsxvalues intsconfig.jsonfiles to override esbuild's globaljsxsetting to keep the ability for multiple files within the same build to use different transforms. -
useDefineForClassFieldsbehavior has changed (#2584, #2993)Class fields in TypeScript look like this (
xis a class field):class Foo { x = 123 }TypeScript has legacy behavior that uses assignment semantics instead of define semantics for class fields when
useDefineForClassFieldsis enabled (in which case class fields in TypeScript behave differently than they do in JavaScript, which is arguably "wrong").This legacy behavior exists because TypeScript added class fields to TypeScript before they were added to JavaScript. The TypeScript team decided to go with assignment semantics and shipped their implementation. Much later on TC39 decided to go with define semantics for class fields in JavaScript instead. This behaves differently if the base class has a setter with the same name:
class Base { set x(_) { console.log('x:', _) } } // useDefineForClassFields: false class AssignSemantics extends Base { constructor() { super() this.x = 123 } } // useDefineForClassFields: true class DefineSemantics extends Base { constructor() { super() Object.defineProperty(this, 'x', { value: 123 }) } } console.log( new AssignSemantics().x, // Calls the setter new DefineSemantics().x // Doesn't call the setter )When you run
tsc, the value ofuseDefineForClassFieldsdefaults tofalsewhen it's not specified and thetargetintsconfig.jsonis present but earlier thanES2022. This sort of makes sense because the class field language feature was added in ES2022, so before ES2022 class fields didn't exist (and thus TypeScript's legacy behavior is active). However, TypeScript'stargetsetting currently defaults toES3which unfortunately means that theuseDefineForClassFieldssetting currently defaults to false (i.e. to "wrong"). In other words if you runtscwith all default settings, class fields will behave incorrectly.Previously esbuild tried to do what
tscdid. That meant esbuild's version ofuseDefineForClassFieldswasfalseby default, and was alsofalseif esbuild's--target=was present but earlier thanes2022. However, TypeScript's legacy class field behavior is becoming increasingly irrelevant and people who expect class fields in TypeScript to work like they do in JavaScript are confused when they use esbuild with default settings. It's also confusing that the behavior of class fields would change if you changed the language target (even though that's exactly how TypeScript works).So with this release, esbuild will now only use the information in
tsconfig.jsonto determine whetheruseDefineForClassFieldsis true or not. SpecificallyuseDefineForClassFieldswill be respected if present, otherwise it will befalseiftargetis present intsconfig.jsonand isES2021or earlier, otherwise it will betrue. Targets passed to esbuild's--target=setting will no longer affectuseDefineForClassFields.Note that this means different directories in your build can have different values for this setting since esbuild allows different directories to have different
tsconfig.jsonfiles within the same build. This should let you migrate your code one directory at a time without esbuild's--target=setting affecting the semantics of your code. -
Add support for
verbatimModuleSyntaxfrom TypeScript 5.0TypeScript 5.0 added a new option called
verbatimModuleSyntaxthat deprecates and replaces two older options,preserveValueImportsandimportsNotUsedAsValues. SettingverbatimModuleSyntaxto true intsconfig.jsontells esbuild to not drop unused import statements. Specifically esbuild now treats"verbatimModuleSyntax": trueas if you had specified both"preserveValueImports": trueand"importsNotUsedAsValues": "preserve". -
Add multiple inheritance for
tsconfig.jsonfrom TypeScript 5.0TypeScript 5.0 now allows multiple inheritance for
tsconfig.jsonfiles. You can now pass an array of filenames via theextendsparameter and yourtsconfig.jsonwill start off containing properties from all of those configuration files, in order. This release of esbuild adds support for this new TypeScript feature. -
Remove support for
moduleSuffixes(#2395)The community has requested that esbuild remove support for TypeScript's
moduleSuffixesfeature, so it has been removed in this release. Instead you can use esbuild's--resolve-extensions=feature to select which module suffix you want to build with. -
Apply
--tsconfig=overrides tostdinand virtual files (#385, #2543)When you override esbuild's automatic
tsconfig.jsonfile detection with--tsconfig=to pass a specifictsconfig.jsonfile, esbuild previously didn't apply these settings to source code passed via thestdinAPI option or to TypeScript files from plugins that weren't in thefilenamespace. This release changes esbuild's behavior so that settings fromtsconfig.jsonalso apply to these source code files as well. -
Support
--tsconfig-raw=in build API calls (#943, #2440)Previously if you wanted to override esbuild's automatic
tsconfig.jsonfile detection, you had to create a newtsconfig.jsonfile and pass the file name to esbuild via the--tsconfig=flag. With this release, you can now optionally use--tsconfig-raw=instead to pass the contents oftsconfig.jsonto esbuild directly instead of passing the file name. For example, you can now use--tsconfig-raw={"compilerOptions":{"experimentalDecorators":true}}to enable TypeScript experimental decorators directly using a command-line flag (assuming you escape the quotes correctly using your current shell's quoting rules). The--tsconfig-raw=flag previously only worked with transform API calls but with this release, it now works with build API calls too. -
Ignore all
tsconfig.jsonfiles innode_modules(#276, #2386)This changes esbuild's behavior that applies
tsconfig.jsonto all files in the subtree of the directory containingtsconfig.json. In version 0.12.7, esbuild started ignoringtsconfig.jsonfiles insidenode_modulesfolders. The rationale is that people typically do this by mistake and that doing this intentionally is a rare use case that doesn't need to be supported. However, this change only applied to certain syntax-specific settings (e.g.jsxFactory) but did not apply to path resolution settings (e.g.paths). With this release, esbuild will now ignore alltsconfig.jsonfiles innode_modulesinstead of only ignoring certain settings. -
Ignore
tsconfig.jsonwhen resolving paths withinnode_modules(#2481)Previously fields in
tsconfig.jsonrelated to path resolution (e.g.paths) were respected for all files in the subtree containing thattsconfig.jsonfile, even within a nestednode_modulessubdirectory. This meant that a project'spathssettings could potentially affect any bundled packages. With this release, esbuild will no longer usetsconfig.jsonsettings during path resolution inside nestednode_modulessubdirectories. -
Prefer
.jsover.tswithinnode_modules(#3019)The default list of implicit extensions that esbuild will try appending to import paths contains
.tsbefore.js. This makes it possible to bundle TypeScript projects that reference other files in the project using extension-less imports (e.g../some-fileto load./some-file.tsinstead of./some-file.js). However, this behavior is undesirable withinnode_modulesdirectories. Some package authors publish both their original TypeScript code and their compiled JavaScript code side-by-side. In these cases, esbuild should arguably be using the compiled JavaScript files instead of the original TypeScript files because the TypeScript compilation settings for files within the package should be determined by the package author, not the user of esbuild. So with this release, esbuild will now prefer implicit.jsextensions over.tswhen searching for import paths withinnode_modules.
These changes are intended to improve esbuild's compatibility with
tscand reduce the number of unfortunate behaviors regardingtsconfig.jsonand esbuild. -
-
Add a workaround for bugs in Safari 16.2 and earlier (#3072)
Safari's JavaScript parser had a bug (which has now been fixed) where at least something about unary/binary operators nested inside default arguments nested inside either a function or class expression was incorrectly considered a syntax error if that expression was the target of a property assignment. Here are some examples that trigger this Safari bug:
❱ x(function (y = -1) {}.z = 2) SyntaxError: Left hand side of operator '=' must be a reference. ❱ x(class { f(y = -1) {} }.z = 2) SyntaxError: Left hand side of operator '=' must be a reference.It's not clear what the exact conditions are that trigger this bug. However, a workaround for this bug appears to be to post-process your JavaScript to wrap any in function and class declarations that are the direct target of a property access expression in parentheses. That's the workaround that UglifyJS applies for this issue: mishoo/UglifyJS#2056. So that's what esbuild now does starting with this release:
// Original code x(function (y = -1) {}.z = 2, class { f(y = -1) {} }.z = 2) // Old output (with --minify --target=safari16.2) x(function(c=-1){}.z=2,class{f(c=-1){}}.z=2); // New output (with --minify --target=safari16.2) x((function(c=-1){}).z=2,(class{f(c=-1){}}).z=2);This fix is not enabled by default. It's only enabled when
--target=contains Safari 16.2 or earlier, such as with--target=safari16.2. You can also explicitly enable or disable this specific transform (calledfunction-or-class-property-access) with--supported:function-or-class-property-access=false. -
Fix esbuild's TypeScript type declarations to forbid unknown properties (#3089)
Version 0.17.0 of esbuild introduced a specific form of function overloads in the TypeScript type definitions for esbuild's API calls that looks like this:
interface TransformOptions { legalComments?: 'none' | 'inline' | 'eof' | 'external' } interface TransformResult<ProvidedOptions extends TransformOptions = TransformOptions> { legalComments: string | (ProvidedOptions['legalComments'] extends 'external' ? never : undefined) } declare function transformSync<ProvidedOptions extends TransformOptions>(input: string, options?: ProvidedOptions): TransformResult<ProvidedOptions> declare function transformSync(input: string, options?: TransformOptions): TransformResultThis more accurately reflects how esbuild's JavaScript API behaves. The result object returned by
transformSynconly has thelegalCommentsproperty if you passlegalComments: 'external':// These have type "string | undefined" transformSync('').legalComments transformSync('', { legalComments: 'eof' }).legalComments // This has type "string" transformSync('', { legalComments: 'external' }).legalCommentsHowever, this form of function overloads unfortunately allows typos (e.g.
egalComments) to pass the type checker without generating an error as TypeScript allows all objects with unknown properties to extendTransformOptions. These typos result in esbuild's API throwing an error at run-time.To prevent typos during type checking, esbuild's TypeScript type definitions will now use a different form that looks like this:
type SameShape<Out, In extends Out> = In & { [Key in Exclude<keyof In, keyof Out>]: never } interface TransformOptions { legalComments?: 'none' | 'inline' | 'eof' | 'external' } interface TransformResult<ProvidedOptions extends TransformOptions = TransformOptions> { legalComments: string | (ProvidedOptions['legalComments'] extends 'external' ? never : undefined) } declare function transformSync<T extends TransformOptions>(input: string, options?: SameShape<TransformOptions, T>): TransformResult<T>This change should hopefully not affect correct code. It should hopefully introduce type errors only for incorrect code.
-
Fix CSS nesting transform for pseudo-elements (#3119)
This release fixes esbuild's CSS nesting transform for pseudo-elements (e.g.
::beforeand::after). The CSS nesting specification says that the nesting selector does not work with pseudo-elements. This can be seen in the example below: esbuild does not carry the parent pseudo-element::beforethrough the nesting selector&. However, that doesn't apply to pseudo-elements that are within the same selector. Previously esbuild had a bug where it considered pseudo-elements in both locations as invalid. This release changes esbuild to only consider those from the parent selector invalid, which should align with the specification:/* Original code */ a, b::before { &.c, &::after { content: 'd'; } } /* Old output (with --target=chrome90) */ a:is(.c, ::after) { content: "d"; } /* New output (with --target=chrome90) */ a.c, a::after { content: "d"; } -
Forbid
&before a type selector in nested CSSThe people behind the work-in-progress CSS nesting specification have very recently decided to forbid nested CSS that looks like
&div. You will have to use eitherdiv&or&:is(div)instead. This release of esbuild has been updated to take this new change into consideration. Doing this now generates a warning. The suggested fix is slightly different depending on where in the overall selector it happened:▲ [WARNING] Cannot use type selector "input" directly after nesting selector "&" [css-syntax-error] example.css:2:3: 2 │ &input { │ ~~~~~ ╵ :is(input) CSS nesting syntax does not allow the "&" selector to come before a type selector. You can wrap this selector in ":is()" as a workaround. This restriction exists to avoid problems with SASS nesting, where the same syntax means something very different that has no equivalent in real CSS (appending a suffix to the parent selector). ▲ [WARNING] Cannot use type selector "input" directly after nesting selector "&" [css-syntax-error] example.css:6:8: 6 │ .form &input { │ ~~~~~~ ╵ input& CSS nesting syntax does not allow the "&" selector to come before a type selector. You can move the "&" to the end of this selector as a workaround. This restriction exists to avoid problems with SASS nesting, where the same syntax means something very different that has no equivalent in real CSS (appending a suffix to the parent selector).
-
Fix CSS transform bugs with nested selectors that start with a combinator (#3096)
This release fixes several bugs regarding transforming nested CSS into non-nested CSS for older browsers. The bugs were due to lack of test coverage for nested selectors with more than one compound selector where they all start with the same combinator. Here's what some problematic cases look like before and after these fixes:
/* Original code */ .foo { > &a, > &b { color: red; } } .bar { > &a, + &b { color: green; } } /* Old output (with --target=chrome90) */ .foo :is(> .fooa, > .foob) { color: red; } .bar :is(> .bara, + .barb) { color: green; } /* New output (with --target=chrome90) */ .foo > :is(a.foo, b.foo) { color: red; } .bar > a.bar, .bar + b.bar { color: green; } -
Fix bug with TypeScript parsing of instantiation expressions followed by
=(#3111)This release fixes esbuild's TypeScript-to-JavaScript conversion code in the case where a potential instantiation expression is followed immediately by a
=token (such that the trailing>becomes a>=token). Previously esbuild considered that to still be an instantiation expression, but the official TypeScript compiler considered it to be a>=operator instead. This release changes esbuild's interpretation to match TypeScript. This edge case currently appears to be problematic for other TypeScript-to-JavaScript converters as well:Original code TypeScript esbuild 0.17.18 esbuild 0.17.19 Sucrase Babel x<y>=a<b<c>>()x<y>=a();x=a();x<y>=a();x=a()Invalid left-hand side in assignment expression -
Avoid removing unrecognized directives from the directive prologue when minifying (#3115)
The directive prologue in JavaScript is a sequence of top-level string expressions that come before your code. The only directives that JavaScript engines currently recognize are
use strictand sometimesuse asm. However, the people behind React have made up their own directive for their own custom dialect of JavaScript. Previously esbuild only preserved theuse strictdirective when minifying, although you could still write React JavaScript with esbuild using something like--banner:js="'your directive here';". With this release, you can now put arbitrary directives in the entry point and esbuild will preserve them in its minified output:// Original code 'use wtf'; console.log(123) // Old output (with --minify) console.log(123); // New output (with --minify) "use wtf";console.log(123);Note that this means esbuild will no longer remove certain stray top-level strings when minifying. This behavior is an intentional change because these stray top-level strings are actually part of the directive prologue, and could potentially have semantics assigned to them (as was the case with React).
-
Improved minification of binary shift operators
With this release, esbuild's minifier will now evaluate the
<<and>>>operators if the resulting code would be shorter:// Original code console.log(10 << 10, 10 << 20, -123 >>> 5, -123 >>> 10); // Old output (with --minify) console.log(10<<10,10<<20,-123>>>5,-123>>>10); // New output (with --minify) console.log(10240,10<<20,-123>>>5,4194303);
-
Fix non-default JSON import error with
export {} from(#3070)This release fixes a bug where esbuild incorrectly identified statements of the form
export { default as x } from "y" assert { type: "json" }as a non-default import. The bug did not affect code of the formimport { default as x } from ...(only code that used theexportkeyword). -
Fix a crash with an invalid subpath import (#3067)
Previously esbuild could crash when attempting to generate a friendly error message for an invalid subpath import (i.e. an import starting with
#). This happened because esbuild originally only supported theexportsfield and the code for that error message was not updated when esbuild later added support for theimportsfield. This crash has been fixed.
-
Fix CSS nesting transform for top-level
&(#3052)Previously esbuild could crash with a stack overflow when lowering CSS nesting rules with a top-level
&, such as in the code below. This happened because esbuild's CSS nesting transform didn't handle top-level&, causing esbuild to inline the top-level selector into itself. This release handles top-level&by replacing it with the:scopepseudo-class:/* Original code */ &, a { .b { color: red; } } /* New output (with --target=chrome90) */ :is(:scope, a) .b { color: red; } -
Support
exportsinpackage.jsonforextendsintsconfig.json(#3058)TypeScript 5.0 added the ability to use
extendsintsconfig.jsonto reference a path in a package whosepackage.jsonfile contains anexportsmap that points to the correct location. This doesn't automatically work in esbuild becausetsconfig.jsonaffects esbuild's path resolution, so esbuild's normal path resolution logic doesn't apply.This release adds support for doing this by adding some additional code that attempts to resolve the
extendspath using theexportsfield. The behavior should be similar enough to esbuild's main path resolution logic to work as expected.Note that esbuild always treats this
extendsimport as arequire()import since that's what TypeScript appears to do. Specifically therequirecondition will be active and theimportcondition will be inactive. -
Fix watch mode with
NODE_PATH(#3062)Node has a rarely-used feature where you can extend the set of directories that node searches for packages using the
NODE_PATHenvironment variable. While esbuild supports this too, previously a bug prevented esbuild's watch mode from picking up changes to imported files that were contained directly in aNODE_PATHdirectory. You're supposed to useNODE_PATHfor packages, but some people abuse this feature by putting files in that directory instead (e.g.node_modules/some-file.jsinstead ofnode_modules/some-pkg/some-file.js). The watch mode bug happens when you do this because esbuild first tries to readsome-file.jsas a directory and then as a file. Watch mode was incorrectly waiting forsome-file.jsto become a valid directory. This release fixes this edge case bug by changing watch mode to watchsome-file.jsas a file when this happens.
-
Fix CSS nesting transform for triple-nested rules that start with a combinator (#3046)
This release fixes a bug with esbuild where triple-nested CSS rules that start with a combinator were not transformed correctly for older browsers. Here's an example of such a case before and after this bug fix:
/* Original input */ .a { color: red; > .b { color: green; > .c { color: blue; } } } /* Old output (with --target=chrome90) */ .a { color: red; } .a > .b { color: green; } .a .b > .c { color: blue; } /* New output (with --target=chrome90) */ .a { color: red; } .a > .b { color: green; } .a > .b > .c { color: blue; } -
Support
--injectwith a file loaded using thecopyloader (#3041)This release now allows you to use
--injectwith a file that is loaded using thecopyloader. Thecopyloader copies the imported file to the output directory verbatim and rewrites the path in theimportstatement to point to the copied output file. When used with--inject, this means the injected file will be copied to the output directory as-is and a bareimportstatement for that file will be inserted in any non-copy output files that esbuild generates.Note that since esbuild doesn't parse the contents of copied files, esbuild will not expose any of the export names as usable imports when you do this (in the way that esbuild's
--injectfeature is typically used). However, any side-effects that the injected file has will still occur.
-
Allow keywords as type parameter names in mapped types (#3033)
TypeScript allows type keywords to be used as parameter names in mapped types. Previously esbuild incorrectly treated this as an error. Code that does this is now supported:
type Foo = 'a' | 'b' | 'c' type A = { [keyof in Foo]: number } type B = { [infer in Foo]: number } type C = { [readonly in Foo]: number } -
Add annotations for re-exported modules in node (#2486, #3029)
Node lets you import named imports from a CommonJS module using ESM import syntax. However, the allowed names aren't derived from the properties of the CommonJS module. Instead they are derived from an arbitrary syntax-only analysis of the CommonJS module's JavaScript AST.
To accommodate node doing this, esbuild's ESM-to-CommonJS conversion adds a special non-executable "annotation" for node that describes the exports that node should expose in this scenario. It takes the form
0 && (module.exports = { ... })and comes at the end of the file (0 && exprmeansexpris never evaluated).Previously esbuild didn't do this for modules re-exported using the
export * fromsyntax. Annotations for these re-exports will now be added starting with this release:// Original input export { foo } from './foo' export * from './bar' // Old output (with --format=cjs --platform=node) ... 0 && (module.exports = { foo }); // New output (with --format=cjs --platform=node) ... 0 && (module.exports = { foo, ...require("./bar") });Note that you need to specify both
--format=cjsand--platform=nodeto get these node-specific annotations. -
Avoid printing an unnecessary space in between a number and a
.(#3026)JavaScript typically requires a space in between a number token and a
.token to avoid the.being interpreted as a decimal point instead of a member expression. However, this space is not required if the number token itself contains a decimal point, an exponent, or uses a base other than 10. This release of esbuild now avoids printing the unnecessary space in these cases:// Original input foo(1000 .x, 0 .x, 0.1 .x, 0.0001 .x, 0xFFFF_0000_FFFF_0000 .x) // Old output (with --minify) foo(1e3 .x,0 .x,.1 .x,1e-4 .x,0xffff0000ffff0000 .x); // New output (with --minify) foo(1e3.x,0 .x,.1.x,1e-4.x,0xffff0000ffff0000.x); -
Fix server-sent events with live reload when writing to the file system root (#3027)
This release fixes a bug where esbuild previously failed to emit server-sent events for live reload when
outdirwas the file system root, such as/. This happened because/is the only path on Unix that cannot have a trailing slash trimmed from it, which was fixed by improved path handling.
-
Allow the TypeScript 5.0
constmodifier in object type declarations (#3021)The new TypeScript 5.0
constmodifier was added to esbuild in version 0.17.5, and works with classes, functions, and arrow expressions. However, support for it wasn't added to object type declarations (e.g. interfaces) due to an oversight. This release adds support for these cases, so the following TypeScript 5.0 code can now be built with esbuild:interface Foo { <const T>(): T } type Bar = { new <const T>(): T } -
Implement preliminary lowering for CSS nesting (#1945)
Chrome has implemented the new CSS nesting specification in version 112, which is currently in beta but will become stable very soon. So CSS nesting is now a part of the web platform!
This release of esbuild can now transform nested CSS syntax into non-nested CSS syntax for older browsers. The transformation relies on the
:is()pseudo-class in many cases, so the transformation is only guaranteed to work when targeting browsers that support:is()(e.g. Chrome 88+). You'll need to set esbuild'stargetto the browsers you intend to support to tell esbuild to do this transformation. You will get a warning if you use CSS nesting syntax with atargetwhich includes older browsers that don't support:is().The lowering transformation looks like this:
/* Original input */ a.btn { color: #333; &:hover { color: #444 } &:active { color: #555 } } /* New output (with --target=chrome88) */ a.btn { color: #333; } a.btn:hover { color: #444; } a.btn:active { color: #555; }More complex cases may generate the
:is()pseudo-class:/* Original input */ div, p { .warning, .error { padding: 20px; } } /* New output (with --target=chrome88) */ :is(div, p) :is(.warning, .error) { padding: 20px; }In addition, esbuild now has a special warning message for nested style rules that start with an identifier. This isn't allowed in CSS because the syntax would be ambiguous with the existing declaration syntax. The new warning message looks like this:
▲ [WARNING] A nested style rule cannot start with "p" because it looks like the start of a declaration [css-syntax-error] <stdin>:1:7: 1 │ main { p { margin: auto } } │ ^ ╵ :is(p) To start a nested style rule with an identifier, you need to wrap the identifier in ":is(...)" to prevent the rule from being parsed as a declaration.Keep in mind that the transformation in this release is a preliminary implementation. CSS has many features that interact in complex ways, and there may be some edge cases that don't work correctly yet.
-
Minification now removes unnecessary
&CSS nesting selectorsThis release introduces the following CSS minification optimizations:
/* Original input */ a { font-weight: bold; & { color: blue; } & :hover { text-decoration: underline; } } /* Old output (with --minify) */ a{font-weight:700;&{color:#00f}& :hover{text-decoration:underline}} /* New output (with --minify) */ a{font-weight:700;:hover{text-decoration:underline}color:#00f} -
Minification now removes duplicates from CSS selector lists
This release introduces the following CSS minification optimization:
/* Original input */ div, div { color: red } /* Old output (with --minify) */ div,div{color:red} /* New output (with --minify) */ div{color:red}
-
Work around an issue with
NODE_PATHand Go's WebAssembly internals (#3001)Go's WebAssembly implementation returns
EINVALinstead ofENOTDIRwhen using thereaddirsyscall on a file. This messes up esbuild's implementation of node's module resolution algorithm since encounteringENOTDIRcauses esbuild to continue its search (since it's a normal condition) while other encountering other errors causes esbuild to fail with an I/O error (since it's an unexpected condition). You can encounter this issue in practice if you use node's legacyNODE_PATHfeature to tell esbuild to resolve node modules in a custom directory that was not installed by npm. This release works around this problem by convertingEINVALintoENOTDIRfor thereaddirsyscall. -
Fix a minification bug with CSS
@layerrules that have parsing errors (#3016)CSS at-rules require either a
{}block or a semicolon at the end. Omitting both of these causes esbuild to treat the rule as an unknown at-rule. Previous releases of esbuild had a bug that incorrectly removed unknown at-rules without any children during minification if the at-rule token matched an at-rule that esbuild can handle. Specifically cssnano can generate@layerrules with parsing errors, and empty@layerrules cannot be removed because they have side effects (@layerdidn't exist when esbuild's CSS support was added, so esbuild wasn't written to handle this). This release changes esbuild to no longer discard@layerrules with parsing errors when minifying (the rule@layer chas a parsing error):/* Original input */ @layer a { @layer b { @layer c } } /* Old output (with --minify) */ @layer a.b; /* New output (with --minify) */ @layer a.b.c; -
Unterminated strings in CSS are no longer an error
The CSS specification provides rules for handling parsing errors. One of those rules is that user agents must close strings upon reaching the end of a line (i.e., before an unescaped line feed, carriage return or form feed character), but then drop the construct (declaration or rule) in which the string was found. For example:
p { color: green; font-family: 'Courier New Times color: red; color: green; }...would be treated the same as:
p { color: green; color: green; }...because the second declaration (from
font-familyto the semicolon aftercolor: red) is invalid and is dropped.Previously using this CSS with esbuild failed to build due to a syntax error, even though the code can be interpreted by a browser. With this release, the code now produces a warning instead of an error, and esbuild prints the invalid CSS such that it stays invalid in the output:
/* esbuild's new non-minified output: */ p { color: green; font-family: 'Courier New Times color: red; color: green; }/* esbuild's new minified output: */ p{font-family:'Courier New Times color: red;color:green}
-
Fix a crash when parsing inline TypeScript decorators (#2991)
Previously esbuild's TypeScript parser crashed when parsing TypeScript decorators if the definition of the decorator was inlined into the decorator itself:
@(function sealed(constructor: Function) { Object.seal(constructor); Object.seal(constructor.prototype); }) class Foo {}This crash was not noticed earlier because this edge case did not have test coverage. The crash is fixed in this release.
-
Fix the
aliasfeature to always prefer the longest match (#2963)It's possible to configure conflicting aliases such as
--alias:a=band--alias:a/c=d, which is ambiguous for the import patha/c/x(since it could map to eitherb/c/xord/x). Previously esbuild would pick the first matchingalias, which would non-deterministically pick between one of the possible matches. This release fixes esbuild to always deterministically pick the longest possible match. -
Minify calls to some global primitive constructors (#2962)
With this release, esbuild's minifier now replaces calls to
Boolean/Number/String/BigIntwith equivalent shorter code when relevant:// Original code console.log( Boolean(a ? (b | c) !== 0 : (c & d) !== 0), Number(e ? '1' : '2'), String(e ? '1' : '2'), BigInt(e ? 1n : 2n), ) // Old output (with --minify) console.log(Boolean(a?(b|c)!==0:(c&d)!==0),Number(e?"1":"2"),String(e?"1":"2"),BigInt(e?1n:2n)); // New output (with --minify) console.log(!!(a?b|c:c&d),+(e?"1":"2"),e?"1":"2",e?1n:2n); -
Adjust some feature compatibility tables for node (#2940)
This release makes the following adjustments to esbuild's internal feature compatibility tables for node, which tell esbuild which versions of node are known to support all aspects of that feature:
class-private-brand-checks: node v16.9+ => node v16.4+ (a decrease)hashbang: node v12.0+ => node v12.5+ (an increase)optional-chain: node v16.9+ => node v16.1+ (a decrease)template-literal: node v4+ => node v10+ (an increase)
Each of these adjustments was identified by comparing against data from the
node-compat-tablepackage and was manually verified using old node executables downloaded from https://nodejs.org/download/release/.
-
Update esbuild's handling of CSS nesting to match the latest specification changes (#1945)
The syntax for the upcoming CSS nesting feature has recently changed. The
@nestprefix that was previously required in some cases is now gone, and nested rules no longer have to start with&(as long as they don't start with an identifier or function token).This release updates esbuild's pass-through handling of CSS nesting syntax to match the latest specification changes. So you can now use esbuild to bundle CSS containing nested rules and try them out in a browser that supports CSS nesting (which includes nightly builds of both Chrome and Safari).
However, I'm not implementing lowering of nested CSS to non-nested CSS for older browsers yet. While the syntax has been decided, the semantics are still in flux. In particular, there is still some debate about changing the fundamental way that CSS nesting works. For example, you might think that the following CSS is equivalent to a
.outer .inner button { ... }rule:.inner button { .outer & { color: red; } }But instead it's actually equivalent to a
.outer :is(.inner button) { ... }rule which unintuitively also matches the following DOM structure:<div class="inner"> <div class="outer"> <button></button> </div> </div>The
:is()behavior is preferred by browser implementers because it's more memory-efficient, but the straightforward translation into a.outer .inner button { ... }rule is preferred by developers used to the existing CSS preprocessing ecosystem (e.g. SASS). It seems premature to commit esbuild to specific semantics for this syntax at this time given the ongoing debate. -
Fix cross-file CSS rule deduplication involving
url()tokens (#2936)Previously cross-file CSS rule deduplication didn't handle
url()tokens correctly. These tokens contain references to import paths which may be internal (i.e. in the bundle) or external (i.e. not in the bundle). When comparing twourl()tokens for equality, the underlying import paths should be compared instead of their references. This release of esbuild fixesurl()token comparisons. One side effect is that@font-facerules should now be deduplicated correctly across files:/* Original code */ @import "data:text/css, \ @import 'http://example.com/style.css'; \ @font-face { src: url(http://example.com/font.ttf) }"; @import "data:text/css, \ @font-face { src: url(http://example.com/font.ttf) }"; /* Old output (with --bundle --minify) */ @import"http://example.com/style.css";@font-face{src:url(http://example.com/font.ttf)}@font-face{src:url(http://example.com/font.ttf)} /* New output (with --bundle --minify) */ @import"http://example.com/style.css";@font-face{src:url(http://example.com/font.ttf)}
-
Parse rest bindings in TypeScript types (#2937)
Previously esbuild was unable to parse the following valid TypeScript code:
let tuple: (...[e1, e2, ...es]: any) => anyThis release includes support for parsing code like this.
-
Fix TypeScript code translation for certain computed
declareclass fields (#2914)In TypeScript, the key of a computed
declareclass field should only be preserved if there are no decorators for that field. Previously esbuild always preserved the key, but esbuild will now remove the key to match the output of the TypeScript compiler:// Original code declare function dec(a: any, b: any): any declare const removeMe: unique symbol declare const keepMe: unique symbol class X { declare [removeMe]: any @dec declare [keepMe]: any } // Old output var _a; class X { } removeMe, _a = keepMe; __decorateClass([ dec ], X.prototype, _a, 2); // New output var _a; class X { } _a = keepMe; __decorateClass([ dec ], X.prototype, _a, 2); -
Fix a crash with path resolution error generation (#2913)
In certain situations, a module containing an invalid import path could previously cause esbuild to crash when it attempts to generate a more helpful error message. This crash has been fixed.
-
Fix a minification bug with non-ASCII identifiers (#2910)
This release fixes a bug with esbuild where non-ASCII identifiers followed by a keyword were incorrectly not separated by a space. This bug affected both the
inandinstanceofkeywords. Here's an example of the fix:// Original code π in a // Old output (with --minify --charset=utf8) πin a; // New output (with --minify --charset=utf8) π in a; -
Fix a regression with esbuild's WebAssembly API in version 0.17.6 (#2911)
Version 0.17.6 of esbuild updated the Go toolchain to version 1.20.0. This had the unfortunate side effect of increasing the amount of stack space that esbuild uses (presumably due to some changes to Go's WebAssembly implementation) which could cause esbuild's WebAssembly-based API to crash with a stack overflow in cases where it previously didn't crash. One such case is the package
grapheme-splitterwhich contains code that looks like this:if ( (0x0300 <= code && code <= 0x036F) || (0x0483 <= code && code <= 0x0487) || (0x0488 <= code && code <= 0x0489) || (0x0591 <= code && code <= 0x05BD) || // ... many hundreds of lines later ... ) { return; }This edge case involves a chain of binary operators that results in an AST over 400 nodes deep. Normally this wouldn't be a problem because Go has growable call stacks, so the call stack would just grow to be as large as needed. However, WebAssembly byte code deliberately doesn't expose the ability to manipulate the stack pointer, so Go's WebAssembly translation is forced to use the fixed-size WebAssembly call stack. So esbuild's WebAssembly implementation is vulnerable to stack overflow in cases like these.
It's not unreasonable for this to cause a stack overflow, and for esbuild's answer to this problem to be "don't write code like this." That's how many other AST-manipulation tools handle this problem. However, it's possible to implement AST traversal using iteration instead of recursion to work around limited call stack space. This version of esbuild implements this code transformation for esbuild's JavaScript parser and printer, so esbuild's WebAssembly implementation is now able to process the
grapheme-splitterpackage (at least when compiled with Go 1.20.0 and run with node's WebAssembly implementation).
-
Change esbuild's parsing of TypeScript instantiation expressions to match TypeScript 4.8+ (#2907)
This release updates esbuild's implementation of instantiation expression erasure to match microsoft/TypeScript#49353. The new rules are as follows (copied from TypeScript's PR description):
When a potential type argument list is followed by
- a line break,
- an
(token, - a template literal string, or
- any token except
<or>that isn't the start of an expression,
we consider that construct to be a type argument list. Otherwise we consider the construct to be a
<relational expression followed by a>relational expression. -
Ignore
sideEffects: falsefor imported CSS files (#1370, #1458, #2905)This release ignores the
sideEffectsannotation inpackage.jsonfor CSS files that are imported into JS files using esbuild'scssloader. This means that these CSS files are no longer be tree-shaken.Importing CSS into JS causes esbuild to automatically create a CSS entry point next to the JS entry point containing the bundled CSS. Previously packages that specified some form of
"sideEffects": falsecould potentially cause esbuild to consider one or more of the JS files on the import path to the CSS file to be side-effect free, which would result in esbuild removing that CSS file from the bundle. This was problematic because the removal of that CSS is outwardly observable, since all CSS is global, so it was incorrect for previous versions of esbuild to tree-shake CSS files imported into JS files. -
Add constant folding for certain additional equality cases (#2394, #2895)
This release adds constant folding for expressions similar to the following:
// Original input console.log( null === 'foo', null === undefined, null == undefined, false === 0, false == 0, 1 === true, 1 == true, ) // Old output console.log( null === "foo", null === void 0, null == void 0, false === 0, false == 0, 1 === true, 1 == true ); // New output console.log( false, false, true, false, true, false, true );
-
Fix a CSS parser crash on invalid CSS (#2892)
Previously the following invalid CSS caused esbuild's parser to crash:
@media screenThe crash was caused by trying to construct a helpful error message assuming that there was an opening
{token, which is not the case here. This release fixes the crash. -
Inline TypeScript enums that are referenced before their declaration
Previously esbuild inlined enums within a TypeScript file from top to bottom, which meant that references to TypeScript enum members were only inlined within the same file if they came after the enum declaration. With this release, esbuild will now inline enums even when they are referenced before they are declared:
// Original input export const foo = () => Foo.FOO const enum Foo { FOO = 0 } // Old output (with --tree-shaking=true) export const foo = () => Foo.FOO; var Foo = /* @__PURE__ */ ((Foo2) => { Foo2[Foo2["FOO"] = 0] = "FOO"; return Foo2; })(Foo || {}); // New output (with --tree-shaking=true) export const foo = () => 0 /* FOO */;This makes esbuild's TypeScript output smaller and faster when processing code that does this. I noticed this issue when I ran the TypeScript compiler's source code through esbuild's bundler. Now that the TypeScript compiler is going to be bundled with esbuild in the upcoming TypeScript 5.0 release, improvements like this will also improve the TypeScript compiler itself!
-
Fix esbuild installation on Arch Linux (#2785, #2812, #2865)
Someone made an unofficial
esbuildpackage for Linux that adds theESBUILD_BINARY_PATH=/usr/bin/esbuildenvironment variable to the user's default environment. This breaks all npm installations of esbuild for users with this unofficial Linux package installed, which has affected many people. Most (all?) people who encounter this problem haven't even installed this unofficial package themselves; instead it was installed for them as a dependency of another Linux package. The problematic change to add theESBUILD_BINARY_PATHenvironment variable was reverted in the latest version of this unofficial package. However, old versions of this unofficial package are still there and will be around forever. With this release,ESBUILD_BINARY_PATHis now ignored by esbuild's install script when it's set to the value/usr/bin/esbuild. This should unbreak using npm to installesbuildin these problematic Linux environments.Note: The
ESBUILD_BINARY_PATHvariable is an undocumented way to override the location of esbuild's binary when esbuild's npm package is installed, which is necessary to substitute your own locally-built esbuild binary when debugging esbuild's npm package. It's only meant for very custom situations and should absolutely not be forced on others by default, especially without their knowledge. I may remove the code in esbuild's installer that readsESBUILD_BINARY_PATHin the future to prevent these kinds of issues. It will unfortunately make debugging esbuild harder. IfESBUILD_BINARY_PATHis ever removed, it will be done in a "breaking change" release.
-
Parse
consttype parameters from TypeScript 5.0The TypeScript 5.0 beta announcement adds
consttype parameters to the language. You can now add theconstmodifier on a type parameter of a function, method, or class like this:type HasNames = { names: readonly string[] }; const getNamesExactly = <const T extends HasNames>(arg: T): T["names"] => arg.names; const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });The type of
namesin the above example isreadonly ["Alice", "Bob", "Eve"]. Marking the type parameter asconstbehaves as if you had writtenas constat every use instead. The above code is equivalent to the following TypeScript, which was the only option before TypeScript 5.0:type HasNames = { names: readonly string[] }; const getNamesExactly = <T extends HasNames>(arg: T): T["names"] => arg.names; const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] } as const);You can read the announcement for more information.
-
Make parsing generic
asyncarrow functions more strict in.tsxfilesPreviously esbuild's TypeScript parser incorrectly accepted the following code as valid:
let fn = async <T> () => {};The official TypeScript parser rejects this code because it thinks it's the identifier
asyncfollowed by a JSX element starting with<T>. So with this release, esbuild will now reject this syntax in.tsxfiles too. You'll now have to add a comma after the type parameter to get generic arrow functions like this to parse in.tsxfiles:let fn = async <T,> () => {}; -
Allow the
inandouttype parameter modifiers on class expressionsTypeScript 4.7 added the
inandoutmodifiers on the type parameters of classes, interfaces, and type aliases. However, while TypeScript supported them on both class expressions and class statements, previously esbuild only supported them on class statements due to an oversight. This release now allows these modifiers on class expressions too:declare let Foo: any; Foo = class <in T> { }; Foo = class <out T> { }; -
Update
enumconstant folding for TypeScript 5.0TypeScript 5.0 contains an updated definition of what it considers a constant expression:
An expression is considered a constant expression if it is
- a number or string literal,
- a unary
+,-, or~applied to a numeric constant expression, - a binary
+,-,*,/,%,**,<<,>>,>>>,|,&,^applied to two numeric constant expressions, - a binary
+applied to two constant expressions whereof at least one is a string, - a template expression where each substitution expression is a constant expression,
- a parenthesized constant expression,
- a dotted name (e.g.
x.y.z) that references aconstvariable with a constant expression initializer and no type annotation, - a dotted name that references an enum member with an enum literal type, or
- a dotted name indexed by a string literal (e.g.
x.y["z"]) that references an enum member with an enum literal type.
This impacts esbuild's implementation of TypeScript's
const enumfeature. With this release, esbuild will now attempt to follow these new rules. For example, you can now initialize anenummember with a template literal expression that contains a numeric constant:// Original input const enum Example { COUNT = 100, ERROR = `Expected ${COUNT} items`, } console.log( Example.COUNT, Example.ERROR, ) // Old output (with --tree-shaking=true) var Example = /* @__PURE__ */ ((Example2) => { Example2[Example2["COUNT"] = 100] = "COUNT"; Example2[Example2["ERROR"] = `Expected ${100 /* COUNT */} items`] = "ERROR"; return Example2; })(Example || {}); console.log( 100 /* COUNT */, Example.ERROR ); // New output (with --tree-shaking=true) console.log( 100 /* COUNT */, "Expected 100 items" /* ERROR */ );These rules are not followed exactly due to esbuild's limitations. The rule about dotted references to
constvariables is not followed both because esbuild's enum processing is done in an isolated module setting and because doing so would potentially require esbuild to use a type system, which it doesn't have. For example:// The TypeScript compiler inlines this but esbuild doesn't: declare const x = 'foo' const enum Foo { X = x } console.log(Foo.X)Also, the rule that requires converting numbers to a string currently only followed for 32-bit signed integers and non-finite numbers. This is done to avoid accidentally introducing a bug if esbuild's number-to-string operation doesn't exactly match the behavior of a real JavaScript VM. Currently esbuild's number-to-string constant folding is conservative for safety.
-
Forbid definite assignment assertion operators on class methods
In TypeScript, class methods can use the
?optional property operator but not the!definite assignment assertion operator (while class fields can use both):class Foo { // These are valid TypeScript a? b! x?() {} // This is invalid TypeScript y!() {} }Previously esbuild incorrectly allowed the definite assignment assertion operator with class methods. This will no longer be allowed starting with this release.
-
Implement HTTP
HEADrequests in serve mode (#2851)Previously esbuild's serve mode only responded to HTTP
GETrequests. With this release, esbuild's serve mode will also respond to HTTPHEADrequests, which are just like HTTPGETrequests except that the body of the response is omitted. -
Permit top-level await in dead code branches (#2853)
Adding top-level await to a file has a few consequences with esbuild:
- It causes esbuild to assume that the input module format is ESM, since top-level await is only syntactically valid in ESM. That prevents you from using
moduleandexportsfor exports and also enables strict mode, which disables certain syntax and changes how function hoisting works (among other things). - This will cause esbuild to fail the build if either top-level await isn't supported by your language target (e.g. it's not supported in ES2021) or if top-level await isn't supported by the chosen output format (e.g. it's not supported with CommonJS).
- Doing this will prevent you from using
require()on this file or on any file that imports this file (even indirectly), since therequire()function doesn't return a promise and so can't represent top-level await.
This release relaxes these rules slightly: rules 2 and 3 will now no longer apply when esbuild has identified the code branch as dead code, such as when it's behind an
if (false)check. This should make it possible to use esbuild to convert code into different output formats that only uses top-level await conditionally. This release does not relax rule 1. Top-level await will still cause esbuild to unconditionally consider the input module format to be ESM, even when the top-levelawaitis in a dead code branch. This is necessary because whether the input format is ESM or not affects the whole file, not just the dead code branch. - It causes esbuild to assume that the input module format is ESM, since top-level await is only syntactically valid in ESM. That prevents you from using
-
Fix entry points where the entire file name is the extension (#2861)
Previously if you passed esbuild an entry point where the file extension is the entire file name, esbuild would use the parent directory name to derive the name of the output file. For example, if you passed esbuild a file
./src/.tsthen the output name would besrc.js. This bug happened because esbuild first strips the file extension to get./src/and then joins the path with the working directory to get the absolute path (e.g.join("/working/dir", "./src/")gives/working/dir/src). However, the join operation also canonicalizes the path which strips the trailing/. Later esbuild uses the "base name" operation to extract the name of the output file. Since there is no trailing/, esbuild returns"src"as the base name instead of"", which causes esbuild to incorrectly include the directory name in the output file name. This release fixes this bug by deferring the stripping of the file extension until after all path manipulations have been completed. So now the file./src/.tswill generate an output file named.js. -
Support replacing property access expressions with inject
At a high level, this change means the
injectfeature can now replace all of the same kinds of names as thedefinefeature. Soinjectis basically now a more powerful version ofdefine, instead of previously only being able to do some of the things thatdefinecould do.Soem background is necessary to understand this change if you aren't already familiar with the
injectfeature. Theinjectfeature lets you replace references to global variable with a shim. It works like this:- Put the shim in its own file
- Export the shim as the name of the global variable you intend to replace
- Pass the file to esbuild using the
injectfeature
For example, if you inject the following file using
--inject:./injected.js:// injected.js let processShim = { cwd: () => '/' } export { processShim as process }Then esbuild will replace all references to
processwith theprocessShimvariable, which will causeprocess.cwd()to return'/'. This feature is sort of abusing the ESM export alias syntax to specify the mapping of global variables to shims. But esbuild works this way because using this syntax for that purpose is convenient and terse.However, if you wanted to replace a property access expression, the process was more complicated and not as nice. You would have to:
- Put the shim in its own file
- Export the shim as some random name
- Pass the file to esbuild using the
injectfeature - Use esbuild's
definefeature to map the property access expression to the random name you made in step 2
For example, if you inject the following file using
--inject:./injected2.js --define:process.cwd=someRandomName:// injected2.js let cwdShim = () => '/' export { cwdShim as someRandomName }Then esbuild will replace all references to
process.cwdwith thecwdShimvariable, which will also causeprocess.cwd()to return'/'(but which this time will not mess with other references toprocess, which might be desirable).With this release, using the inject feature to replace a property access expression is now as simple as using it to replace an identifier. You can now use JavaScript's "arbitrary module namespace identifier names" feature to specify the property access expression directly using a string literal. For example, if you inject the following file using
--inject:./injected3.js:// injected3.js let cwdShim = () => '/' export { cwdShim as 'process.cwd' }Then esbuild will now replace all references to
process.cwdwith thecwdShimvariable, which will also causeprocess.cwd()to return'/'(but which will also not mess with other references toprocess).In addition to inserting a shim for a global variable that doesn't exist, another use case is replacing references to static methods on global objects with cached versions to both minify them better and to make access to them potentially faster. For example:
// Injected file let cachedMin = Math.min let cachedMax = Math.max export { cachedMin as 'Math.min', cachedMax as 'Math.max', } // Original input function clampRGB(r, g, b) { return { r: Math.max(0, Math.min(1, r)), g: Math.max(0, Math.min(1, g)), b: Math.max(0, Math.min(1, b)), } } // Old output (with --minify) function clampRGB(a,t,m){return{r:Math.max(0,Math.min(1,a)),g:Math.max(0,Math.min(1,t)),b:Math.max(0,Math.min(1,m))}} // New output (with --minify) var a=Math.min,t=Math.max;function clampRGB(h,M,m){return{r:t(0,a(1,h)),g:t(0,a(1,M)),b:t(0,a(1,m))}}
-
Fix incorrect CSS minification for certain rules (#2838)
Certain rules such as
@mediacould previously be minified incorrectly. Due to a typo in the duplicate rule checker, two known@-rules that share the same hash code were incorrectly considered to be equal. This problem was made worse by the rule hashing code considering two unknown declarations (such as CSS variables) to have the same hash code, which also isn't optimal from a performance perspective. Both of these issues have been fixed:/* Original input */ @media (prefers-color-scheme: dark) { body { --VAR-1: #000; } } @media (prefers-color-scheme: dark) { body { --VAR-2: #000; } } /* Old output (with --minify) */ @media (prefers-color-scheme: dark){body{--VAR-2: #000}} /* New output (with --minify) */ @media (prefers-color-scheme: dark){body{--VAR-1: #000}}@media (prefers-color-scheme: dark){body{--VAR-2: #000}}
-
Add
onDisposeto the plugin API (#2140, #2205)If your plugin wants to perform some cleanup after it's no longer going to be used, you can now use the
onDisposeAPI to register a callback for cleanup-related tasks. For example, if a plugin starts a long-running child process then it may want to terminate that process when the plugin is discarded. Previously there was no way to do this. Here's an example:let examplePlugin = { name: 'example', setup(build) { build.onDispose(() => { console.log('This plugin is no longer used') }) }, }These
onDisposecallbacks will be called after everybuild()call regardless of whether the build failed or not as well as after the firstdispose()call on a given build context.
-
Make it possible to cancel a build (#2725)
The context object introduced in version 0.17.0 has a new
cancel()method. You can use it to cancel a long-running build so that you can start a new one without needing to wait for the previous one to finish. When this happens, the previous build should always have at least one error and have no output files (i.e. it will be a failed build).Using it might look something like this:
-
JS:
let ctx = await esbuild.context({ // ... }) let rebuildWithTimeLimit = timeLimit => { let timeout = setTimeout(() => ctx.cancel(), timeLimit) return ctx.rebuild().finally(() => clearTimeout(timeout)) } let build = await rebuildWithTimeLimit(500) -
Go:
ctx, err := api.Context(api.BuildOptions{ // ... }) if err != nil { return } rebuildWithTimeLimit := func(timeLimit time.Duration) api.BuildResult { t := time.NewTimer(timeLimit) go func() { <-t.C ctx.Cancel() }() result := ctx.Rebuild() t.Stop() return result } build := rebuildWithTimeLimit(500 * time.Millisecond)
This API is a quick implementation and isn't maximally efficient, so the build may continue to do some work for a little bit before stopping. For example, I have added stop points between each top-level phase of the bundler and in the main module graph traversal loop, but I haven't added fine-grained stop points within the internals of the linker. How quickly esbuild stops can be improved in future releases. This means you'll want to wait for
cancel()and/or the previousrebuild()to finish (i.e. await the returned promise in JavaScript) before starting a new build, otherwiserebuild()will give you the just-canceled build that still hasn't ended yet. Note thatonEndcallbacks will still be run regardless of whether or not the build was canceled. -
-
Fix server-sent events without
servedir(#2827)The server-sent events for live reload were incorrectly using
servedirto calculate the path to modified output files. This means events couldn't be sent whenservedirwasn't specified. This release uses the internal output directory (which is always present) instead ofservedir(which might be omitted), so live reload should now work whenservediris not specified. -
Custom entry point output paths now work with the
copyloader (#2828)Entry points can optionally provide custom output paths to change the path of the generated output file. For example,
esbuild foo=abc.js bar=xyz.js --outdir=outgenerates the filesout/foo.jsandout/bar.js. However, this previously didn't work when using thecopyloader due to an oversight. This bug has been fixed. For example, you can now doesbuild foo=abc.html bar=xyz.html --outdir=out --loader:.html=copyto generate the filesout/foo.htmlandout/bar.html. -
The JS API can now take an array of objects (#2828)
Previously it was not possible to specify two entry points with the same custom output path using the JS API, although it was possible to do this with the Go API and the CLI. This will not cause a collision if both entry points use different extensions (e.g. if one uses
.jsand the other uses.css). You can now pass the JS API an array of objects to work around this API limitation:// The previous API didn't let you specify duplicate output paths let result = await esbuild.build({ entryPoints: { // This object literal contains a duplicate key, so one is ignored 'dist': 'foo.js', 'dist': 'bar.css', }, }) // You can now specify duplicate output paths as an array of objects let result = await esbuild.build({ entryPoints: [ { in: 'foo.js', out: 'dist' }, { in: 'bar.css', out: 'dist' }, ], })
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.16.0 or ~0.16.0. See npm's documentation about semver for more information.
At a high level, the breaking changes in this release fix some long-standing issues with the design of esbuild's incremental, watch, and serve APIs. This release also introduces some exciting new features such as live reloading. In detail:
-
Move everything related to incremental builds to a new
contextAPI (#1037, #1606, #2280, #2418)This change removes the
incrementalandwatchoptions as well as theserve()method, and introduces a newcontext()method. The context method takes the same arguments as thebuild()method but only validates its arguments and does not do an initial build. Instead, builds can be triggered using therebuild(),watch(), andserve()methods on the returned context object. The new context API looks like this:// Create a context for incremental builds const context = await esbuild.context({ entryPoints: ['app.ts'], bundle: true, }) // Manually do an incremental build const result = await context.rebuild() // Enable watch mode await context.watch() // Enable serve mode await context.serve() // Dispose of the context context.dispose()The switch to the context API solves a major issue with the previous API which is that if the initial build fails, a promise is thrown in JavaScript which prevents you from accessing the returned result object. That prevented you from setting up long-running operations such as watch mode when the initial build contained errors. It also makes tearing down incremental builds simpler as there is now a single way to do it instead of three separate ways.
In addition, this release also makes some subtle changes to how incremental builds work. Previously every call to
rebuild()started a new build. If you weren't careful, then builds could actually overlap. This doesn't cause any problems with esbuild itself, but could potentially cause problems with plugins (esbuild doesn't even give you a way to identify which overlapping build a given plugin callback is running on). Overlapping builds also arguably aren't useful, or at least aren't useful enough to justify the confusion and complexity that they bring. With this release, there is now only ever a single active build per context. Callingrebuild()before the previous rebuild has finished now "merges" with the existing rebuild instead of starting a new build. -
Allow using
watchandservetogether (#805, #1650, #2576)Previously it was not possible to use watch mode and serve mode together. The rationale was that watch mode is one way of automatically rebuilding your project and serve mode is another (since serve mode automatically rebuilds on every request). However, people want to combine these two features to make "live reloading" where the browser automatically reloads the page when files are changed on the file system.
This release now allows you to use these two features together. You can only call the
watch()andserve()APIs once each per context, but if you call them together on the same context then esbuild will automatically rebuild both when files on the file system are changed and when the server serves a request. -
Support "live reloading" through server-sent events (#802)
Server-sent events are a simple way to pass one-directional messages asynchronously from the server to the client. Serve mode now provides a
/esbuildendpoint with anchangeevent that triggers every time esbuild's output changes. So you can now implement simple "live reloading" (i.e. reloading the page when a file is edited and saved) like this:new EventSource('/esbuild').addEventListener('change', () => location.reload())The event payload is a JSON object with the following shape:
interface ChangeEvent { added: string[] removed: string[] updated: string[] }This JSON should also enable more complex live reloading scenarios. For example, the following code hot-swaps changed CSS
<link>tags in place without reloading the page (but still reloads when there are other types of changes):new EventSource('/esbuild').addEventListener('change', e => { const { added, removed, updated } = JSON.parse(e.data) if (!added.length && !removed.length && updated.length === 1) { for (const link of document.getElementsByTagName("link")) { const url = new URL(link.href) if (url.host === location.host && url.pathname === updated[0]) { const next = link.cloneNode() next.href = updated[0] + '?' + Math.random().toString(36).slice(2) next.onload = () => link.remove() link.parentNode.insertBefore(next, link.nextSibling) return } } } location.reload() })Implementing live reloading like this has a few known caveats:
-
These events only trigger when esbuild's output changes. They do not trigger when files unrelated to the build being watched are changed. If your HTML file references other files that esbuild doesn't know about and those files are changed, you can either manually reload the page or you can implement your own live reloading infrastructure instead of using esbuild's built-in behavior.
-
The
EventSourceAPI is supposed to automatically reconnect for you. However, there's a bug in Firefox that breaks this if the server is ever temporarily unreachable: https://bugzilla.mozilla.org/show_bug.cgi?id=1809332. Workarounds are to use any other browser, to manually reload the page if this happens, or to write more complicated code that manually closes and re-creates theEventSourceobject if there is a connection error. I'm hopeful that this bug will be fixed. -
Browser vendors have decided to not implement HTTP/2 without TLS. This means that each
/esbuildevent source will take up one of your precious 6 simultaneous per-domain HTTP/1.1 connections. So if you open more than six HTTP tabs that use this live-reloading technique, you will be unable to use live reloading in some of those tabs (and other things will likely also break). The workaround is to enable HTTPS, which is now possible to do in esbuild itself (see below).
-
-
Add built-in support for HTTPS (#2169)
You can now tell esbuild's built-in development server to use HTTPS instead of HTTP. This is sometimes necessary because browser vendors have started making modern web features unavailable to HTTP websites. Previously you had to put a proxy in front of esbuild to enable HTTPS since esbuild's development server only supported HTTP. But with this release, you can now enable HTTPS with esbuild without an additional proxy.
To enable HTTPS with esbuild:
-
Generate a self-signed certificate. There are many ways to do this. Here's one way, assuming you have
opensslinstalled:openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 9999 -nodes -subj /CN=127.0.0.1 -
Add
--keyfile=key.pemand--certfile=cert.pemto your esbuild development server command -
Click past the scary warning in your browser when you load your page
If you have more complex needs than this, you can still put a proxy in front of esbuild and use that for HTTPS instead. Note that if you see the message "Client sent an HTTP request to an HTTPS server" when you load your page, then you are using the incorrect protocol. Replace
http://withhttps://in your browser's URL bar.Keep in mind that esbuild's HTTPS support has nothing to do with security. The only reason esbuild now supports HTTPS is because browsers have made it impossible to do local development with certain modern web features without jumping through these extra hoops. Please do not use esbuild's development server for anything that needs to be secure. It's only intended for local development and no considerations have been made for production environments whatsoever.
-
-
Better support copying
index.htmlinto the output directory (#621, #1771)Right now esbuild only supports JavaScript and CSS as first-class content types. Previously this meant that if you were building a website with a HTML file, a JavaScript file, and a CSS file, you could use esbuild to build the JavaScript file and the CSS file into the output directory but not to copy the HTML file into the output directory. You needed a separate
cpcommand for that.Or so I thought. It turns out that the
copyloader added in version 0.14.44 of esbuild is sufficient to have esbuild copy the HTML file into the output directory as well. You can add something likeindex.html --loader:.html=copyand esbuild will copyindex.htmlinto the output directory for you. The benefits of this are a) you don't need a separatecpcommand and b) theindex.htmlfile will automatically be re-copied when esbuild is in watch mode and the contents ofindex.htmlare edited. This also goes for other non-HTML file types that you might want to copy.This pretty much already worked. The one thing that didn't work was that esbuild's built-in development server previously only supported implicitly loading
index.html(e.g. loading/about/index.htmlwhen you visit/about/) whenindex.htmlexisted on the file system. Previously esbuild didn't support implicitly loadingindex.htmlif it was a build result. That bug has been fixed with this release so it should now be practical to use thecopyloader to do this. -
Fix
onEndnot being called in serve mode (#1384)Previous releases had a bug where plugin
onEndcallbacks weren't called when using the top-levelserve()API. This API no longer exists and the internals have been reimplemented such thatonEndcallbacks should now always be called at the end of every build. -
Incremental builds now write out build results differently (#2104)
Previously build results were always written out after every build. However, this could cause the output directory to fill up with files from old builds if code splitting was enabled, since the file names for code splitting chunks contain content hashes and old files were not deleted.
With this release, incremental builds in esbuild will now delete old output files from previous builds that are no longer relevant. Subsequent incremental builds will also no longer overwrite output files whose contents haven't changed since the previous incremental build.
-
The
onRebuildwatch mode callback was removed (#980, #2499)Previously watch mode accepted an
onRebuildcallback which was called whenever watch mode rebuilt something. This was not great in practice because if you are running code after a build, you likely want that code to run after every build, not just after the second and subsequent builds. This release removes option to provide anonRebuildcallback. You can create a plugin with anonEndcallback instead. TheonEndplugin API already exists, and is a way to run some code after every build. -
You can now return errors from
onEnd(#2625)It's now possible to add additional build errors and/or warnings to the current build from within your
onEndcallback by returning them in an array. This is identical to how theonStartcallback already works. The evaluation ofonEndcallbacks have been moved around a bit internally to make this possible.Note that the build will only fail (i.e. reject the promise) if the additional errors are returned from
onEnd. Adding additional errors to the result object that's passed toonEndwon't affect esbuild's behavior at all. -
Print URLs and ports from the Go and JS APIs (#2393)
Previously esbuild's CLI printed out something like this when serve mode is active:
> Local: http://127.0.0.1:8000/ > Network: http://192.168.0.1:8000/The CLI still does this, but now the JS and Go serve mode APIs will do this too. This only happens when the log level is set to
verbose,debug, orinfobut not when it's set towarning,error, orsilent.
Upgrade guide for existing code:
-
Rebuild (a.k.a. incremental build):
Before:
const result = await esbuild.build({ ...buildOptions, incremental: true }); builds.push(result); for (let i = 0; i < 4; i++) builds.push(await result.rebuild()); await result.rebuild.dispose(); // To free resourcesAfter:
const ctx = await esbuild.context(buildOptions); for (let i = 0; i < 5; i++) builds.push(await ctx.rebuild()); await ctx.dispose(); // To free resourcesPreviously the first build was done differently than subsequent builds. Now both the first build and subsequent builds are done using the same API.
-
Serve:
Before:
const serveResult = await esbuild.serve(serveOptions, buildOptions); ... serveResult.stop(); await serveResult.wait; // To free resourcesAfter:
const ctx = await esbuild.context(buildOptions); const serveResult = await ctx.serve(serveOptions); ... await ctx.dispose(); // To free resources -
Watch:
Before:
const result = await esbuild.build({ ...buildOptions, watch: true }); ... result.stop(); // To free resourcesAfter:
const ctx = await esbuild.context(buildOptions); await ctx.watch(); ... await ctx.dispose(); // To free resources -
Watch with
onRebuild:Before:
const onRebuild = (error, result) => { if (error) console.log('subsequent build:', error); else console.log('subsequent build:', result); }; try { const result = await esbuild.build({ ...buildOptions, watch: { onRebuild } }); console.log('first build:', result); ... result.stop(); // To free resources } catch (error) { console.log('first build:', error); }After:
const plugins = [{ name: 'my-plugin', setup(build) { let count = 0; build.onEnd(result => { if (count++ === 0) console.log('first build:', result); else console.log('subsequent build:', result); }); }, }]; const ctx = await esbuild.context({ ...buildOptions, plugins }); await ctx.watch(); ... await ctx.dispose(); // To free resourcesThe
onRebuildfunction has now been removed. The replacement is to make a plugin with anonEndcallback.Previously
onRebuilddid not fire for the first build (only for subsequent builds). This was usually problematic, so usingonEndinstead ofonRebuildis likely less error-prone. But if you need to emulate the old behavior ofonRebuildthat ignores the first build, then you'll need to manually count and ignore the first build in your plugin (as demonstrated above).
Notice how all of these API calls are now done off the new context object. You should now be able to use all three kinds of incremental builds (rebuild, serve, and watch) together on the same context object. Also notice how calling dispose on the context is now the common way to discard the context and free resources in all of these situations.
-
Fix additional comment-related regressions (#2814)
This release fixes more edge cases where the new comment preservation behavior that was added in 0.16.14 could introduce syntax errors. Specifically:
x = () => (/* comment */ {}) for ((/* comment */ let).x of y) ; function *f() { yield (/* comment */class {}) }These cases caused esbuild to generate code with a syntax error in version 0.16.14 or above. These bugs have now been fixed.
-
Fix a regression caused by comment preservation (#2805)
The new comment preservation behavior that was added in 0.16.14 introduced a regression where comments in certain locations could cause esbuild to omit certain necessary parentheses in the output. The outermost parentheses were incorrectly removed for the following syntax forms, which then introduced syntax errors:
(/* comment */ { x: 0 }).x; (/* comment */ function () { })(); (/* comment */ class { }).prototype;This regression has been fixed.
-
Add
formatto input files in the JSON metafile dataWhen
--metafileis enabled, input files may now have an additionalformatfield that indicates the export format used by this file. When present, the value will either becjsfor CommonJS-style exports oresmfor ESM-style exports. This can be useful in bundle analysis.For example, esbuild's new Bundle Size Analyzer now uses this information to visualize whether ESM or CommonJS was used for each directory and file of source code (click on the CJS/ESM bar at the top).
This information is helpful when trying to reduce the size of your bundle. Using the ESM variant of a dependency instead of the CommonJS variant always results in a faster and smaller bundle because it omits CommonJS wrappers, and also may result in better tree-shaking as it allows esbuild to perform tree-shaking at the statement level instead of the module level.
-
Fix a bundling edge case with dynamic import (#2793)
This release fixes a bug where esbuild's bundler could produce incorrect output. The problematic edge case involves the entry point importing itself using a dynamic
import()expression in an imported file, like this:// src/a.js export const A = 42; // src/b.js export const B = async () => (await import(".")).A // src/index.js export * from "./a" export * from "./b" -
Remove new type syntax from type declarations in the
esbuildpackage (#2798)Previously you needed to use TypeScript 4.3 or newer when using the
esbuildpackage from TypeScript code due to the use of a getter in an interface innode_modules/esbuild/lib/main.d.ts. This release removes this newer syntax to allow people with versions of TypeScript as far back as TypeScript 3.5 to use this latest version of theesbuildpackage. Here is change that was made to esbuild's type declarations:export interface OutputFile { /** "text" as bytes */ contents: Uint8Array; /** "contents" as text (changes automatically with "contents") */ - get text(): string; + readonly text: string; }
-
Preserve some comments in expressions (#2721)
Various tools give semantic meaning to comments embedded inside of expressions. For example, Webpack and Vite have special "magic comments" that can be used to affect code splitting behavior:
import(/* webpackChunkName: "foo" */ '../foo'); import(/* @vite-ignore */ dynamicVar); new Worker(/* webpackChunkName: "bar" */ new URL("../bar.ts", import.meta.url)); new Worker(new URL('./path', import.meta.url), /* @vite-ignore */ dynamicOptions);Since esbuild can be used as a preprocessor for these tools (e.g. to strip TypeScript types), it can be problematic if esbuild doesn't do additional work to try to retain these comments. Previously esbuild special-cased Webpack comments in these specific locations in the AST. But Vite would now like to use similar comments, and likely other tools as well.
So with this release, esbuild now will attempt to preserve some comments inside of expressions in more situations than before. This behavior is mainly intended to preserve these special "magic comments" that are meant for other tools to consume, although esbuild will no longer only preserve Webpack-specific comments so it should now be tool-agnostic. There is no guarantee that all such comments will be preserved (especially when
--minify-syntaxis enabled). So this change does not mean that esbuild is now usable as a code formatter. In particular comment preservation is more likely to happen with leading comments than with trailing comments. You should put comments that you want to be preserved before the relevant expression instead of after it. Also note that this change does not retain any more statement-level comments than before (i.e. comments not embedded inside of expressions). Comment preservation is not enabled when--minify-whitespaceis enabled (which is automatically enabled when you use--minify).
-
Publish a new bundle visualization tool
While esbuild provides bundle metadata via the
--metafileflag, previously esbuild left analysis of it completely up to third-party tools (well, outside of the rudimentary--analyzeflag). However, the esbuild website now has a built-in bundle visualization tool:You can pass
--metafileto esbuild to output bundle metadata, then upload that JSON file to this tool to visualize your bundle. This is helpful for answering questions such as:- Which packages are included in my bundle?
- How did a specific file get included?
- How small did a specific file compress to?
- Was a specific file tree-shaken or not?
I'm publishing this tool because I think esbuild should provide some answer to "how do I visualize my bundle" without requiring people to reach for third-party tools. At the moment the tool offers two types of visualizations: a radial "sunburst chart" and a linear "flame chart". They serve slightly different but overlapping use cases (e.g. the sunburst chart is more keyboard-accessible while the flame chart is easier with the mouse). This tool may continue to evolve over time.
-
Fix
--metafileand--mangle-cachewith--watch(#1357)The CLI calls the Go API and then also writes out the metafile and/or mangle cache JSON files if those features are enabled. This extra step is necessary because these files are returned by the Go API as in-memory strings. However, this extra step accidentally didn't happen for all builds after the initial build when watch mode was enabled. This behavior used to work but it was broken in version 0.14.18 by the introduction of the mangle cache feature. This release fixes the combination of these features, so the metafile and mangle cache features should now work with watch mode. This behavior was only broken for the CLI, not for the JS or Go APIs.
-
Add an
originalfield to the metafileThe metadata file JSON now has an additional field: each import in an input file now contains the pre-resolved path in the
originalfield in addition to the post-resolved path in thepathfield. This means it's now possible to run certain additional analysis over your bundle. For example, you should be able to use this to detect when the same package subpath is represented multiple times in the bundle, either because multiple versions of a package were bundled or because a package is experiencing the dual-package hazard.
-
Loader defaults to
jsfor extensionless files (#2776)Certain packages contain files without an extension. For example, the
yargspackage contains the fileyargs/yargswhich has no extension. Node, Webpack, and Parcel can all understand code that importsyargs/yargsbecause they assume that the file is JavaScript. However, esbuild was previously unable to understand this code because it relies on the file extension to tell it how to interpret the file. With this release, esbuild will now assume files without an extension are JavaScript files. This can be customized by setting the loader for""(the empty string, representing files without an extension) to another loader. For example, if you want files without an extension to be treated as CSS instead, you can do that like this:-
CLI:
esbuild --bundle --loader:=css -
JS:
esbuild.build({ bundle: true, loader: { '': 'css' }, }) -
Go:
api.Build(api.BuildOptions{ Bundle: true, Loader: map[string]api.Loader{"": api.LoaderCSS}, })
In addition, the
"type"field inpackage.jsonfiles now only applies to files with an explicit.js,.jsx,.ts, or.tsxextension. Previously it was incorrectly applied by esbuild to all files that had an extension other than.mjs,.mts,.cjs, or.ctsincluding extensionless files. So for example an extensionless file in a"type": "module"package is now treated as CommonJS instead of ESM. -
-
Avoid a syntax error in the presence of direct
eval(#2761)The behavior of nested
functiondeclarations in JavaScript depends on whether the code is run in strict mode or not. It would be problematic if esbuild preserved nestedfunctiondeclarations in its output because then the behavior would depend on whether the output was run in strict mode or not instead of respecting the strict mode behavior of the original source code. To avoid this, esbuild transforms nestedfunctiondeclarations to preserve the intended behavior of the original source code regardless of whether the output is run in strict mode or not:// Original code if (true) { function foo() {} console.log(!!foo) foo = null console.log(!!foo) } console.log(!!foo) // Transformed code if (true) { let foo2 = function() { }; var foo = foo2; console.log(!!foo2); foo2 = null; console.log(!!foo2); } console.log(!!foo);In the above example, the original code should print
true false truebecause it's not run in strict mode (it doesn't contain"use strict"and is not an ES module). The code that esbuild generates has been transformed such that it printstrue false trueregardless of whether it's run in strict mode or not.However, this transformation is impossible if the code contains direct
evalbecause directeval"poisons" all containing scopes by preventing anything in those scopes from being renamed. That prevents esbuild from splitting up accesses tofoointo two separate variables with different names. Previously esbuild still did this transformation but with two variables both namedfoo, which is a syntax error. With this release esbuild will now skip doing this transformation when directevalis present to avoid generating code with a syntax error. This means that the generated code may no longer behave as intended since the behavior depends on the run-time strict mode setting instead of the strict mode setting present in the original source code. To fix this problem, you will need to remove the use of directeval. -
Fix a bundling scenario involving multiple symlinks (#2773, #2774)
This release contains a fix for a bundling scenario involving an import path where multiple path segments are symlinks. Previously esbuild was unable to resolve certain import paths in this scenario, but these import paths should now work starting with this release. This fix was contributed by @onebytegone.
-
Change the default "legal comment" behavior again (#2745)
The legal comments feature automatically gathers comments containing
@licenseor@preserveand puts the comments somewhere (either in the generated code or in a separate file). This behavior used to be on by default but was disabled by default in version 0.16.0 because automatically inserting comments is potentially confusing and misleading. These comments can appear to be assigning the copyright of your code to another entity. And this behavior can be especially problematic if it happens automatically by default since you may not even be aware of it happening. For example, if you bundle the TypeScript compiler the preserving legal comments means your source code would contain this comment, which appears to be assigning the copyright of all of your code to Microsoft:/*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */However, people have asked for this feature to be re-enabled by default. To resolve the confusion about what these comments are applying to, esbuild's default behavior will now be to attempt to describe which package the comments are coming from. So while this feature has been re-enabled by default, the output will now look something like this instead:
/*! Bundled license information: typescript/lib/typescript.js: (*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** *) */Note that you can still customize this behavior with the
--legal-comments=flag. For example, you can use--legal-comments=noneto turn this off, or you can use--legal-comments=linkedto put these comments in a separate.LEGAL.txtfile instead. -
Enable
externallegal comments with the transform API (#2390)Previously esbuild's transform API only supported
none,inline, oreoflegal comments. With this release,externallegal comments are now also supported with the transform API. This only applies to the JS and Go APIs, not to the CLI, and looks like this:-
JS:
const { code, legalComments } = await esbuild.transform(input, { legalComments: 'external', }) -
Go:
result := api.Transform(input, api.TransformOptions{ LegalComments: api.LegalCommentsEndOfFile, }) code := result.Code legalComments := result.LegalComments
-
-
Fix duplicate function declaration edge cases (#2757)
The change in the previous release to forbid duplicate function declarations in certain cases accidentally forbid some edge cases that should have been allowed. Specifically duplicate function declarations are forbidden in nested blocks in strict mode and at the top level of modules, but are allowed when they are declared at the top level of function bodies. This release fixes the regression by re-allowing the last case.
-
Allow package subpaths with
alias(#2715)Previously the names passed to the
aliasfeature had to be the name of a package (with or without a package scope). With this release, you can now also use thealiasfeature with package subpaths. So for example you can now create an alias that substitutes@org/pkg/libwith something else.
-
Update to Unicode 15.0.0
The character tables that determine which characters form valid JavaScript identifiers have been updated from Unicode version 14.0.0 to the newly-released Unicode version 15.0.0. I'm not putting an example in the release notes because all of the new characters will likely just show up as little squares since fonts haven't been updated yet. But you can read https://www.unicode.org/versions/Unicode15.0.0/#Summary for more information about the changes.
-
Disallow duplicate lexically-declared names in nested blocks and in strict mode
In strict mode or in a nested block, it's supposed to be a syntax error to declare two symbols with the same name unless all duplicate entries are either
functiondeclarations or allvardeclarations. However, esbuild was overly permissive and allowed this when duplicate entries were eitherfunctiondeclarations orvardeclarations (even if they were mixed). This check has now been made more restrictive to match the JavaScript specification:// JavaScript allows this var a function a() {} { var b var b function c() {} function c() {} } // JavaScript doesn't allow this { var d function d() {} } -
Add a type declaration for the new
emptyloader (#2755)I forgot to add this in the previous release. It has now been added.
This fix was contributed by @fz6m.
-
Add support for the
vflag in regular expression literalsPeople are currently working on adding a
vflag to JavaScript regular expresions. You can read more about this flag here: https://v8.dev/features/regexp-v-flag. This release adds support for parsing this flag, so esbuild will now no longer consider regular expression literals with this flag to be a syntax error. If the target is set to something other thanesnext, esbuild will transform regular expression literals containing this flag into anew RegExp()constructor call so the resulting code doesn't have a syntax error. This enables you to provide a polyfill forRegExpthat implements thevflag to get your code to work at run-time. While esbuild doesn't typically adopt proposals until they're already shipping in a real JavaScript run-time, I'm adding it now because a) esbuild's implementation doesn't need to change as the proposal evolves, b) this isn't really new syntax since regular expression literals already have flags, and c) esbuild's implementation is a trivial pass-through anyway. -
Avoid keeping the name of classes with static
namepropertiesThe
--keep-namesproperty attempts to preserve the original value of thenameproperty for functions and classes even when identifiers are renamed by the minifier or to avoid a name collision. This is currently done by generating code to assign a string to thenameproperty on the function or class object. However, this should not be done for classes with a staticnameproperty since in that case the explicitly-definednameproperty overwrites the automatically-generated class name. With this release, esbuild will now no longer attempt to preserve thenameproperty for classes with a staticnameproperty.
-
Allow plugins to resolve injected files (#2754)
Previously paths passed to the
injectfeature were always interpreted as file system paths. This meant thatonResolveplugins would not be run for them and esbuild's default path resolver would always be used. This meant that theinjectfeature couldn't be used in the browser since the browser doesn't have access to a file system. This release runs paths passed toinjectthrough esbuild's full path resolution pipeline so plugins now have a chance to handle them usingonResolvecallbacks. This makes it possible to write a plugin that makes esbuild'sinjectwork in the browser. -
Add the
emptyloader (#1541, #2753)The new
emptyloader tells esbuild to pretend that a file is empty. So for example--loader:.css=emptyeffectively skips all imports of.cssfiles in JavaScript so that they aren't included in the bundle, sinceimport "./some-empty-file"in JavaScript doesn't bundle anything. You can also use theemptyloader to remove asset references in CSS files. For example--loader:.png=emptycauses esbuild to replace asset references such asurl(image.png)withurl()so that they are no longer included in the resulting style sheet. -
Fix
</script>and</style>escaping for non-default targets (#2748)The change in version 0.16.0 to give control over
</script>escaping via--supported:inline-script=falseor--supported:inline-script=trueaccidentally broke automatic escaping of</script>when an explicittargetsetting is specified. This release restores the correct automatic escaping of</script>(which should not depend on whattargetis set to). -
Enable the
exportsfield withNODE_PATHS(#2752)Node has a rarely-used feature where you can extend the set of directories that node searches for packages using the
NODE_PATHSenvironment variable. While esbuild supports this too, previously it only supported the oldmainfield path resolution but did not support the newexportsfield package resolution. This release makes the path resolution rules the same again for bothnode_modulesdirectories andNODE_PATHSdirectories.
-
Include
fileloader strings in metafile imports (#2731)Bundling a file with the
fileloader copies that file to the output directory and imports a module with the path to the copied file in thedefaultexport. Previously when bundling with thefileloader, there was no reference in the metafile from the JavaScript file containing the path string to the copied file. With this release, there will now be a reference in the metafile in theimportsarray with the kindfile-loader:{ ... "outputs": { "out/image-55CCFTCE.svg": { ... }, "out/entry.js": { "imports": [ + { + "path": "out/image-55CCFTCE.svg", + "kind": "file-loader" + } ], ... } } } -
Fix byte counts in metafile regarding references to other output files (#2071)
Previously files that contained references to other output files had slightly incorrect metadata for the byte counts of input files which contributed to that output file. So for example if
app.jsimportsimage.pngusing the file loader and esbuild generatesout.jsandimage-LSAMBFUD.png, the metadata for how many bytes ofout.jsare fromapp.jswas slightly off (the metadata for the byte count ofout.jswas still correct). The reason is because esbuild substitutes the final paths for references between output files toward the end of the build to handle cyclic references, and the byte counts needed to be adjusted as well during the path substitution. This release fixes these byte counts (specifically thebytesInOutputvalues). -
The alias feature now strips a trailing slash (#2730)
People sometimes add a trailing slash to the name of one of node's built-in modules to force node to import from the file system instead of importing the built-in module. For example, importing
utilimports node's built-in module calledutilbut importingutil/tries to find a package calledutilon the file system. Previously attempting to use esbuild's package alias feature to replace imports toutilwith a specific file would fail because the file path would also gain a trailing slash (e.g. mappingutilto./file.jsturnedutil/into./file.js/). With this release, esbuild will now omit the path suffix if it's a single trailing slash, which should now allow you to successfully apply aliases to these import paths.
-
Do not mark subpath imports as external with
--packages=external(#2741)Node has a feature called subpath imports where special import paths that start with
#are resolved using theimportsfield in thepackage.jsonfile of the enclosing package. The intent of the newly-added--packages=externalsetting is to exclude a package's dependencies from the bundle. Since a package's subpath imports are only accessible within that package, it's wrong for them to be affected by--packages=external. This release changes esbuild so that--packages=externalno longer affects subpath imports. -
Forbid invalid numbers in JSON files
Previously esbuild parsed numbers in JSON files using the same syntax as JavaScript. But starting from this release, esbuild will now parse them with JSON syntax instead. This means the following numbers are no longer allowed by esbuild in JSON files:
- Legacy octal literals (non-zero integers starting with
0) - The
0b,0o, and0xnumeric prefixes - Numbers containing
_such as1_000 - Leading and trailing
.such as0.and.0 - Numbers with a space after the
-such as- 1
- Legacy octal literals (non-zero integers starting with
-
Add external imports to metafile (#905, #1768, #1933, #1939)
External imports now appear in
importsarrays in the metafile (which is present when bundling withmetafile: true) next to normal imports, but additionally haveexternal: trueto set them apart. This applies both to files in theinputssection and theoutputssection. Here's an example:{ "inputs": { "style.css": { "bytes": 83, "imports": [ + { + "path": "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css", + "kind": "import-rule", + "external": true + } ] }, "app.js": { "bytes": 100, "imports": [ + { + "path": "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js", + "kind": "import-statement", + "external": true + }, { "path": "style.css", "kind": "import-statement" } ] } }, "outputs": { "out/app.js": { "imports": [ + { + "path": "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.min.js", + "kind": "require-call", + "external": true + } ], "exports": [], "entryPoint": "app.js", "cssBundle": "out/app.css", "inputs": { "app.js": { "bytesInOutput": 113 }, "style.css": { "bytesInOutput": 0 } }, "bytes": 528 }, "out/app.css": { "imports": [ + { + "path": "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css", + "kind": "import-rule", + "external": true + } ], "inputs": { "style.css": { "bytesInOutput": 0 } }, "bytes": 100 } } }One additional useful consequence of this is that the
importsarray is now populated when bundling is disabled. So you can now use esbuild with bundling disabled to inspect a file's imports.
-
Make it easy to exclude all packages from a bundle (#1958, #1975, #2164, #2246, #2542)
When bundling for node, it's often necessary to exclude npm packages from the bundle since they weren't designed with esbuild bundling in mind and don't work correctly after being bundled. For example, they may use
__dirnameand run-time file system calls to load files, which doesn't work after bundling with esbuild. Or they may compile a native.nodeextension that has similar expectations about the layout of the file system that are no longer true after bundling (even if the.nodeextension is copied next to the bundle).The way to get this to work with esbuild is to use the
--external:flag. For example, thefseventspackage contains a native.nodeextension and shouldn't be bundled. To bundle code that uses it, you can pass--external:fseventsto esbuild to exclude it from your bundle. You will then need to ensure that thefseventspackage is still present when you run your bundle (e.g. by publishing your bundle to npm as a package with a dependency onfsevents).It was possible to automatically do this for all of your dependencies, but it was inconvenient. You had to write some code that read your
package.jsonfile and passed the keys of thedependencies,devDependencies,peerDependencies, and/oroptionalDependenciesmaps to esbuild as external packages (either that or write a plugin to mark all package paths as external). Previously esbuild's recommendation for making this easier was to do--external:./node_modules/*(added in version 0.14.13). However, this was a bad idea because it caused compatibility problems with many node packages as it caused esbuild to mark the post-resolve path as external instead of the pre-resolve path. Doing that could break packages that are published as both CommonJS and ESM if esbuild's bundler is also used to do a module format conversion.With this release, you can now do the following to automatically exclude all packages from your bundle:
-
CLI:
esbuild --bundle --packages=external -
JS:
esbuild.build({ bundle: true, packages: 'external', }) -
Go:
api.Build(api.BuildOptions{ Bundle: true, Packages: api.PackagesExternal, })
Doing
--external:./node_modules/*is still possible and still has the same behavior, but is no longer recommended. I recommend that you use the newpackagesfeature instead. -
-
Fix some subtle bugs with tagged template literals
This release fixes a bug where minification could incorrectly change the value of
thiswithin tagged template literal function calls:// Original code function f(x) { let z = y.z return z`` } // Old output (with --minify) function f(n){return y.z``} // New output (with --minify) function f(n){return(0,y.z)``}This release also fixes a bug where using optional chaining with
--target=es2019or earlier could incorrectly change the value ofthiswithin tagged template literal function calls:// Original code var obj = { foo: function() { console.log(this === obj); } }; (obj?.foo)``; // Old output (with --target=es6) var obj = { foo: function() { console.log(this === obj); } }; (obj == null ? void 0 : obj.foo)``; // New output (with --target=es6) var __freeze = Object.freeze; var __defProp = Object.defineProperty; var __template = (cooked, raw) => __freeze(__defProp(cooked, "raw", { value: __freeze(raw || cooked.slice()) })); var _a; var obj = { foo: function() { console.log(this === obj); } }; (obj == null ? void 0 : obj.foo).call(obj, _a || (_a = __template([""]))); -
Some slight minification improvements
The following minification improvements were implemented:
if (~a !== 0) throw x;=>if (~a) throw x;if ((a | b) !== 0) throw x;=>if (a | b) throw x;if ((a & b) !== 0) throw x;=>if (a & b) throw x;if ((a ^ b) !== 0) throw x;=>if (a ^ b) throw x;if ((a << b) !== 0) throw x;=>if (a << b) throw x;if ((a >> b) !== 0) throw x;=>if (a >> b) throw x;if ((a >>> b) !== 0) throw x;=>if (a >>> b) throw x;if (!!a || !!b) throw x;=>if (a || b) throw x;if (!!a && !!b) throw x;=>if (a && b) throw x;if (a ? !!b : !!c) throw x;=>if (a ? b : c) throw x;
-
Fix binary downloads from the
@esbuild/scope for Deno (#2729)Version 0.16.0 of esbuild moved esbuild's binary executables into npm packages under the
@esbuild/scope, which accidentally broke the binary downloader script for Deno. This release fixes this script so it should now be possible to use esbuild version 0.16.4+ with Deno.
-
Fix a hang with the JS API in certain cases (#2727)
A change that was made in version 0.15.13 accidentally introduced a case when using esbuild's JS API could cause the node process to fail to exit. The change broke esbuild's watchdog timer, which detects if the parent process no longer exists and then automatically exits esbuild. This hang happened when you ran node as a child process with the
stderrstream set topipeinstead ofinherit, in the child process you call esbuild's JS API and passincremental: truebut do not calldispose()on the returnedrebuildobject, and then callprocess.exit(). In that case the parent node process was still waiting for the esbuild process that was created by the child node process to exit. The change made in version 0.15.13 was trying to avoid using Go'ssync.WaitGroupAPI incorrectly because the API is not thread-safe. Instead of doing this, I have now reverted that change and implemented a thread-safe version of thesync.WaitGroupAPI for esbuild to use instead.
-
Fix
process.env.NODE_ENVsubstitution when transforming (#2718)Version 0.16.0 introduced an unintentional regression that caused
process.env.NODE_ENVto be automatically substituted with either"development"or"production"when using esbuild'stransformAPI. This substitution is a necessary feature of esbuild'sbuildAPI because the React framework crashes when you bundle it without doing this. But thetransformAPI is typically used as part of a larger build pipeline so the benefit of esbuild doing this automatically is not as clear, and esbuild previously didn't do this.However, version 0.16.0 switched the default value of the
platformsetting for thetransformAPI fromneutraltobrowser, both to align it with esbuild's documentation (which saysbrowseris the default value) and because escaping the</script>character sequence is now tied to thebrowserplatform (see the release notes for version 0.16.0 for details). That accidentally enabled automatic substitution ofprocess.env.NODE_ENVbecause esbuild always did that for code meant for the browser. To fix this regression, esbuild will now only automatically substituteprocess.env.NODE_ENVwhen using thebuildAPI. -
Prevent
definefrom substituting constants into assignment position (#2719)The
definefeature lets you replace certain expressions with constants. For example, you could use it to replace references to the global property referencewindow.DEBUGwithfalseat compile time, which can then potentially help esbuild remove unused code from your bundle. It's similar to DefinePlugin in Webpack.However, if you write code such as
window.DEBUG = trueand then definedwindow.DEBUGtofalse, esbuild previously generated the outputfalse = truewhich is a syntax error in JavaScript. This behavior is not typically a problem because it doesn't make sense to substitutewindow.DEBUGwith a constant if its value changes at run-time (Webpack'sDefinePluginalso generatesfalse = truein this case). But it can be alarming to have esbuild generate code with a syntax error.So with this release, esbuild will no longer substitute
defineconstants into assignment position to avoid generating code with a syntax error. Instead esbuild will generate a warning, which currently looks like this:▲ [WARNING] Suspicious assignment to defined constant "window.DEBUG" [assign-to-define] example.js:1:0: 1 │ window.DEBUG = true ╵ ~~~~~~~~~~~~ The expression "window.DEBUG" has been configured to be replaced with a constant using the "define" feature. If this expression is supposed to be a compile-time constant, then it doesn't make sense to assign to it here. Or if this expression is supposed to change at run-time, this "define" substitution should be removed. -
Fix a regression with
npm install --no-optional(#2720)Normally when you install esbuild with
npm install, npm itself is the tool that downloads the correct binary executable for the current platform. This happens because of how esbuild's primary package uses npm'soptionalDependenciesfeature. However, if you deliberately disable this withnpm install --no-optionalthen esbuild's install script will attempt to repair the installation by manually downloading and extracting the binary executable from the package that was supposed to be installed.The change in version 0.16.0 to move esbuild's nested packages into the
@esbuild/scope unintentionally broke this logic because of how npm's URL structure is different for scoped packages vs. normal packages. It was actually already broken for a few platforms earlier because esbuild already had packages for some platforms in the@esbuild/scope, but I didn't discover this then because esbuild's integration tests aren't run on all platforms. Anyway, this release contains some changes to the install script that should hopefully get this scenario working again.
This is a hotfix for the previous release.
-
Re-allow importing JSON with the
copyloader using an import assertionThe previous release made it so when
assert { type: 'json' }is present on an import statement, esbuild validated that thejsonloader was used. This is what an import assertion is supposed to do. However, I forgot about the relatively newcopyloader, which sort of behaves as if the import path was marked as external (and thus not loaded at all) except that the file is copied to the output directory and the import path is rewritten to point to the copy. In this case whatever JavaScript runtime ends up running the code is the one to evaluate the import assertion. So esbuild should really allow this case as well. With this release, esbuild now allows both thejsonandcopyloaders when anassert { type: 'json' }import assertion is present.
This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.15.0 or ~0.15.0. See npm's documentation about semver for more information.
-
Move all binary executable packages to the
@esbuild/scopeBinary package executables for esbuild are published as individual packages separate from the main
esbuildpackage so you only have to download the relevant one for the current platform when you install esbuild. This release moves all of these packages under the@esbuild/scope to avoid collisions with 3rd-party packages. It also changes them to a consistent naming scheme that uses theosandcpunames from node.The package name changes are as follows:
@esbuild/linux-loong64=>@esbuild/linux-loong64(no change)esbuild-android-64=>@esbuild/android-x64esbuild-android-arm64=>@esbuild/android-arm64esbuild-darwin-64=>@esbuild/darwin-x64esbuild-darwin-arm64=>@esbuild/darwin-arm64esbuild-freebsd-64=>@esbuild/freebsd-x64esbuild-freebsd-arm64=>@esbuild/freebsd-arm64esbuild-linux-32=>@esbuild/linux-ia32esbuild-linux-64=>@esbuild/linux-x64esbuild-linux-arm=>@esbuild/linux-armesbuild-linux-arm64=>@esbuild/linux-arm64esbuild-linux-mips64le=>@esbuild/linux-mips64elesbuild-linux-ppc64le=>@esbuild/linux-ppc64esbuild-linux-riscv64=>@esbuild/linux-riscv64esbuild-linux-s390x=>@esbuild/linux-s390xesbuild-netbsd-64=>@esbuild/netbsd-x64esbuild-openbsd-64=>@esbuild/openbsd-x64esbuild-sunos-64=>@esbuild/sunos-x64esbuild-wasm=>esbuild-wasm(no change)esbuild-windows-32=>@esbuild/win32-ia32esbuild-windows-64=>@esbuild/win32-x64esbuild-windows-arm64=>@esbuild/win32-arm64esbuild=>esbuild(no change)
Normal usage of the
esbuildandesbuild-wasmpackages should not be affected. These name changes should only affect tools that hard-coded the individual binary executable package names into custom esbuild downloader scripts.This change was not made with performance in mind. But as a bonus, installing esbuild with npm may potentially happen faster now. This is because npm's package installation protocol is inefficient: it always downloads metadata for all past versions of each package even when it only needs metadata about a single version. This makes npm package downloads O(n) in the number of published versions, which penalizes packages like esbuild that are updated regularly. Since most of esbuild's package names have now changed, npm will now need to download much less data when installing esbuild (8.72mb of package manifests before this change → 0.06mb of package manifests after this change). However, this is only a temporary improvement. Installing esbuild will gradually get slower again as further versions of esbuild are published.
-
Publish a shell script that downloads esbuild directly
In addition to all of the existing ways to install esbuild, you can now also download esbuild directly like this:
curl -fsSL https://esbuild.github.io/dl/latest | shThis runs a small shell script that downloads the latest
esbuildbinary executable to the current directory. This can be convenient on systems that don't havenpminstalled or when you just want to get a copy of esbuild quickly without any extra steps. If you want a specific version of esbuild (starting with this version onward), you can provide that version in the URL instead oflatest:curl -fsSL https://esbuild.github.io/dl/v0.16.0 | shNote that the download script needs to be able to access registry.npmjs.org to be able to complete the download. This download script doesn't yet support all of the platforms that esbuild supports because I lack the necessary testing environments. If the download script doesn't work for you because you're on an unsupported platform, please file an issue on the esbuild repo so we can add support for it.
-
Fix some parameter names for the Go API
This release changes some parameter names for the Go API to be consistent with the JavaScript and CLI APIs:
OutExtensions=>OutExtensionJSXMode=>JSX
-
Add additional validation of API parameters
The JavaScript API now does some additional validation of API parameters to catch incorrect uses of esbuild's API. The biggest impact of this is likely that esbuild now strictly only accepts strings with the
defineparameter. This would already have been a type error with esbuild's TypeScript type definitions, but it was previously not enforced for people using esbuild's API JavaScript without TypeScript.The
defineparameter appears at first glance to take a JSON object if you aren't paying close attention, but this actually isn't true. Values fordefineare instead strings of JavaScript code. This means you have to usedefine: { foo: '"bar"' }to replacefoowith the string"bar". Usingdefine: { foo: 'bar' }actually replacesfoowith the identifierbar. Previously esbuild allowed you to passdefine: { foo: false }andfalsewas automatically converted into a string, which made it more confusing to understand whatdefineactually represents. Starting with this release, passing non-string values such as withdefine: { foo: false }will no longer be allowed. You will now have to writedefine: { foo: 'false' }instead. -
Generate shorter data URLs if possible (#1843)
Loading a file with esbuild's
dataurlloader generates a JavaScript module with a data URL for that file in a string as a single default export. Previously the data URLs generated by esbuild all used base64 encoding. However, this is unnecessarily long for most textual data (e.g. SVG images). So with this release, esbuild'sdataurlloader will now use percent encoding instead of base64 encoding if the result will be shorter. This can result in ~25% smaller data URLs for large SVGs. If you want the old behavior, you can use thebase64loader instead and then construct the data URL yourself. -
Avoid marking entry points as external (#2382)
Previously you couldn't specify
--external:*to mark all import paths as external because that also ended up making the entry point itself external, which caused the build to fail. With this release, esbuild'sexternalAPI parameter no longer applies to entry points so using--external:*is now possible.One additional consequence of this change is that the
kindparameter is now required when calling theresolve()function in esbuild's plugin API. Previously thekindparameter defaulted toentry-point, but that no longer interacts withexternalso it didn't seem wise for this to continue to be the default. You now have to specifykindso that the path resolution mode is explicit. -
Disallow non-
defaultimports whenassert { type: 'json' }is presentThere is now standard behavior for importing a JSON file into an ES module using an
importstatement. However, it requires you to place theassert { type: 'json' }import assertion after the import path. This import assertion tells the JavaScript runtime to throw an error if the import does not end up resolving to a JSON file. On the web, the type of a file is determined by theContent-TypeHTTP header instead of by the file extension. The import assertion prevents security problems on the web where a.jsonfile may actually resolve to a JavaScript file containing malicious code, which is likely not expected for an import that is supposed to only contain pure side-effect free data.By default, esbuild uses the file extension to determine the type of a file, so this import assertion is unnecessary with esbuild. However, esbuild's JSON import feature has a non-standard extension that allows you to import top-level properties of the JSON object as named imports. For example, esbuild lets you do this:
import { version } from './package.json'This is useful for tree-shaking when bundling because it means esbuild will only include the the
versionfield ofpackage.jsonin your bundle. This is non-standard behavior though and doesn't match the behavior of what happens when you import JSON in a real JavaScript runtime (after addingassert { type: 'json' }). In a real JavaScript runtime the only thing you can import is thedefaultimport. So with this release, esbuild will now prevent you from importing non-defaultimport names ifassert { type: 'json' }is present. This ensures that code containingassert { type: 'json' }isn't relying on non-standard behavior that won't work everywhere. So the following code is now an error with esbuild when bundling:import { version } from './package.json' assert { type: 'json' }In addition, adding
assert { type: 'json' }to an import statement now means esbuild will generate an error if the loader for the file is anything other thanjson, which is required by the import assertion specification. -
Provide a way to disable automatic escaping of
</script>(#2649)If you inject esbuild's output into a script tag in an HTML file, code containing the literal characters
</script>will cause the tag to be ended early which will break the code:<script> console.log("</script>"); </script>To avoid this, esbuild automatically escapes these strings in generated JavaScript files (e.g.
"</script>"becomes"<\/script>"instead). This also applies to</style>in generated CSS files. Previously this always happened and there wasn't a way to turn this off.With this release, esbuild will now only do this if the
platformsetting is set tobrowser(the default value). Settingplatformtonodeorneutralwill disable this behavior. This behavior can also now be disabled with--supported:inline-script=false(for JS) and--supported:inline-style=false(for CSS). -
Throw an early error if decoded UTF-8 text isn't a
Uint8Array(#2532)If you run esbuild's JavaScript API in a broken JavaScript environment where
new TextEncoder().encode("") instanceof Uint8Arrayis false, then esbuild's API will fail with a confusing serialization error message that makes it seem like esbuild has a bug even though the real problem is that the JavaScript environment itself is broken. This can happen when using the test framework called Jest. With this release, esbuild's API will now throw earlier when it detects that the environment is unable to encode UTF-8 text correctly with an error message that makes it more clear that this is not a problem with esbuild. -
Change the default "legal comment" behavior
The legal comments feature automatically gathers comments containing
@licenseor@preserveand puts the comments somewhere (either in the generated code or in a separate file). People sometimes want this to happen so that the their dependencies' software licenses are retained in the generated output code. By default esbuild puts these comments at the end of the file when bundling. However, people sometimes find this confusing because these comments can be very generic and may not mention which library they come from. So with this release, esbuild will now discard legal comments by default. You now have to opt-in to preserving them if you want this behavior. -
Enable the
modulecondition by default (#2417)Package authors want to be able to use the new
exportsfield inpackage.jsonto provide tree-shakable ESM code for ESM-aware bundlers while simultaneously providing fallback CommonJS code for other cases.Node's proposed way to do this involves using the
importandrequireexport conditions so that you get the ESM code if you use an import statement and the CommonJS code if you use a require call. However, this has a major drawback: if some code in the bundle uses an import statement and other code in the bundle uses a require call, then you'll get two copies of the same package in the bundle. This is known as the dual package hazard and can lead to bloated bundles or even worse to subtle logic bugs.Webpack supports an alternate solution: an export condition called
modulethat takes effect regardless of whether the package was imported using an import statement or a require call. This works because bundlers such as Webpack support importing a ESM using a require call (something node doesn't support). You could already do this with esbuild using--conditions=modulebut you previously had to explicitly enable this. Package authors are concerned that esbuild users won't know to do this and will get suboptimal output with their package, so they have requested for esbuild to do this automatically.So with this release, esbuild will now automatically add the
modulecondition when there aren't any customconditionsalready configured. You can disable this with--conditions=orconditions: [](i.e. explicitly clearing all custom conditions). -
Rename the
masterbranch tomainThe primary branch for this repository was previously called
masterbut is now calledmain. This change mirrors a similar change in many other projects. -
Remove esbuild's
_exit(0)hack for WebAssembly (#714)Node had an unfortunate bug where the node process is unnecessarily kept open while a WebAssembly module is being optimized: https://github.com/nodejs/node/issues/36616. This means cases where running
esbuildshould take a few milliseconds can end up taking many seconds instead.The workaround was to force node to exit by ending the process early. This was done by esbuild in one of two ways depending on the exit code. For non-zero exit codes (i.e. when there is a build error), the
esbuildcommand could just callprocess.kill(process.pid)to avoid the hang. But for zero exit codes, esbuild had to load a N-API native node extension that calls the operating system'sexit(0)function.However, this problem has essentially been fixed in node starting with version 18.3.0. So I have removed this hack from esbuild. If you are using an earlier version of node with
esbuild-wasmand you don't want theesbuildcommand to hang for a while when exiting, you can upgrade to node 18.3.0 or higher to remove the hang.The fix came from a V8 upgrade: this commit enabled dynamic tiering for WebAssembly by default for all projects that use V8's WebAssembly implementation. Previously all functions in the WebAssembly module were optimized in a single batch job but with dynamic tiering, V8 now optimizes individual WebAssembly functions as needed. This avoids unnecessary WebAssembly compilation which allows node to exit on time.
-
Performance improvements for both JS and CSS
This release brings noticeable performance improvements for JS parsing and for CSS parsing and printing. Here's an example benchmark for using esbuild to pretty-print a single large minified CSS file and JS file:
Test case Previous release This release 4.8mb CSS file 19ms 11ms (1.7x faster) 5.8mb JS file 36ms 32ms (1.1x faster) The performance improvements were very straightforward:
-
Identifiers were being scanned using a generic character advancement function instead of using custom inline code. Advancing past each character involved UTF-8 decoding as well as updating multiple member variables. This was sped up using loop that skips UTF-8 decoding entirely and that only updates member variables once at the end. This is faster because identifiers are plain ASCII in the vast majority of cases, so Unicode decoding is almost always unnecessary.
-
CSS identifiers and CSS strings were still being printed one character at a time. Apparently I forgot to move this part of esbuild's CSS infrastructure beyond the proof-of-concept stage. These were both very obvious in the profiler, so I think maybe I have just never profiled esbuild's CSS printing before?
-
There was unnecessary work being done that was related to source maps when source map output was disabled. I likely haven't observed this before because esbuild's benchmarks always have source maps enabled. This work is now disabled when it's not going to be used.
I definitely should have caught these performance issues earlier. Better late than never I suppose.
-
-
Search for missing source map code on the file system (#2711)
Source maps are JSON files that map from compiled code back to the original code. They provide the original source code using two arrays:
sources(required) andsourcesContent(optional). When bundling is enabled, esbuild is able to bundle code with source maps that was compiled by other tools (e.g. with Webpack) and emit source maps that map all the way back to the original code (e.g. before Webpack compiled it).Previously if the input source maps omitted the optional
sourcesContentarray, esbuild would usenullfor the source content in the source map that it generates (since the source content isn't available). However, sometimes the original source code is actually still present on the file system. With this release, esbuild will now try to find the original source code using the path in thesourcesarray and will use that instead ofnullif it was found. -
Fix parsing bug with TypeScript
inferandextends(#2712)This release fixes a bug where esbuild incorrectly failed to parse valid TypeScript code that nests
extendsinsideinferinsideextends, such as in the example below:type A<T> = {}; type B = {} extends infer T extends {} ? A<T> : never;TypeScript code that does this should now be parsed correctly.
-
Use
WebAssembly.instantiateStreamingif available (#1036, #1900)Currently the WebAssembly version of esbuild uses
fetchto downloadesbuild.wasmand thenWebAssembly.instantiateto compile it. There is a newer API calledWebAssembly.instantiateStreamingthat both downloads and compiles at the same time, which can be a performance improvement if both downloading and compiling are slow. With this release, esbuild now attempts to useWebAssembly.instantiateStreamingand falls back to the original approach if that fails.The implementation for this builds on a PR by @lbwa.
-
Preserve Webpack comments inside constructor calls (#2439)
This improves the use of esbuild as a faster TypeScript-to-JavaScript frontend for Webpack, which has special magic comments inside
new Worker()expressions that affect Webpack's behavior.
-
Add a package alias feature (#2191)
With this release, you can now easily substitute one package for another at build time with the new
aliasfeature. For example,--alias:oldpkg=newpkgreplaces all imports ofoldpkgwithnewpkg. One use case for this is easily replacing a node-only package with a browser-friendly package in 3rd-party code that you don't control. These new substitutions happen first before all of esbuild's existing path resolution logic.Note that when an import path is substituted using an alias, the resulting import path is resolved in the working directory instead of in the directory containing the source file with the import path. If needed, the working directory can be set with the
cdcommand when using the CLI or with theabsWorkingDirsetting when using the JS or Go APIs. -
Fix crash when pretty-printing minified JSX with object spread of object literal with computed property (#2697)
JSX elements are translated to JavaScript function calls and JSX element attributes are translated to properties on a JavaScript object literal. These properties are always either strings (e.g. in
<x y />,yis a string) or an object spread (e.g. in<x {...y} />,yis an object spread) because JSX doesn't provide syntax for directly passing a computed property as a JSX attribute. However, esbuild's minifier has a rule that tries to inline object spread with an inline object literal in JavaScript. For example,x = { ...{ y } }is minified tox={y}when minification is enabled. This means that there is a way to generate a non-string non-spread JSX attribute in esbuild's internal representation. One example is with<x {...{ [y]: z }} />. When minification is enabled, esbuild's internal representation of this is something like<x [y]={z} />due to object spread inlining, which is not valid JSX syntax. If this internal representation is then pretty-printed as JSX using--minify --jsx=preserve, esbuild previously crashed when trying to print this invalid syntax. With this release, esbuild will now print<x {...{[y]:z}}/>in this scenario instead of crashing.
-
Remove duplicate CSS rules across files (#2688)
When two or more CSS rules are exactly the same (even if they are not adjacent), all but the last one can safely be removed:
/* Before */ a { color: red; } span { font-weight: bold; } a { color: red; } /* After */ span { font-weight: bold; } a { color: red; }Previously esbuild only did this transformation within a single source file. But with this release, esbuild will now do this transformation across source files, which may lead to smaller CSS output if the same rules are repeated across multiple CSS source files in the same bundle. This transformation is only enabled when minifying (specifically when syntax minification is enabled).
-
Add
denoas a valid value fortarget(#2686)The
targetsetting in esbuild allows you to enable or disable JavaScript syntax features for a given version of a set of target JavaScript VMs. Previously Deno was not one of the JavaScript VMs that esbuild supported withtarget, but it will now be supported starting from this release. For example, versions of Deno older than v1.2 don't support the new||=operator, so adding e.g.--target=deno1.0to esbuild now lets you tell esbuild to transpile||=to older JavaScript. -
Fix the
esbuild-wasmpackage in Node v19 (#2683)A recent change to Node v19 added a non-writable
cryptoproperty to the global object: https://github.com/nodejs/node/pull/44897. This conflicts with Go's WebAssembly shim code, which overwrites the globalcryptoproperty. As a result, all Go-based WebAssembly code that uses the built-in shim (including esbuild) is now broken on Node v19. This release of esbuild fixes the issue by reconfiguring the globalcryptoproperty to be writable before invoking Go's WebAssembly shim code. -
Fix CSS dimension printing exponent confusion edge case (#2677)
In CSS, a dimension token has a numeric "value" part and an identifier "unit" part. For example, the dimension token
32pxhas a value of32and a unit ofpx. The unit can be any valid CSS identifier. The value can be any number in floating-point format including an optional exponent (e.g.-3.14e-0has an exponent ofe-0). The full details of this syntax are here: https://www.w3.org/TR/css-syntax-3/.To maintain the integrity of the dimension token through the printing process, esbuild must handle the edge case where the unit looks like an exponent. One such case is the dimension
1e\32which has the value1and the unite2. It would be bad if this dimension token was printed such that a CSS parser would parse it as a number token with the value1e2instead of a dimension token. The way esbuild currently does this is to escape the leadingein the dimension unit, so esbuild would parse1e\32but print1\65 2(both1e\32and1\65 2represent a dimension token with a value of1and a unit ofe2).However, there is an even narrower edge case regarding this edge case. If the value part of the dimension token itself has an
e, then it's not necessary to escape theein the dimension unit because a CSS parser won't confuse the unit with the exponent even though it looks like one (since a number can only have at most one exponent). This came up because the grammar for the CSSunicode-rangeproperty uses a hack that lets you specify a hexadecimal range without quotes even though CSS has no token for a hexadecimal range. The hack is to allow the hexadecimal range to be parsed as a dimension token and optionally also a number token. Here is the grammar forunicode-range:unicode-range = <urange># <urange> = u '+' <ident-token> '?'* | u <dimension-token> '?'* | u <number-token> '?'* | u <number-token> <dimension-token> | u <number-token> <number-token> | u '+' '?'+and here is an example
unicode-rangedeclaration that was problematic for esbuild:@font-face { unicode-range: U+0e2e-0e2f; }This is parsed as a dimension with a value of
+0e2and a unit ofe-0e2f. This was problematic for esbuild because the unit starts withe-0which could be confused with an exponent when appended after a number, so esbuild was escaping theecharacter in the unit. However, this escaping is unnecessary because in this case the dimension value already has an exponent in it. With this release, esbuild will no longer unnecessarily escape theein the dimension unit in these cases, which should fix the printing ofunicode-rangedeclarations.An aside: You may be wondering why esbuild is trying to escape the
eat all and why it doesn't just pass through the original source code unmodified. The reason why esbuild does this is that, for robustness, esbuild's AST generally tries to omit semantically-unrelated information and esbuild's code printers always try to preserve the semantics of the underlying AST. That way the rest of esbuild's internals can just deal with semantics instead of presentation. They don't have to think about how the AST will be printed when changing the AST. This is the same reason that esbuild's JavaScript AST doesn't have a "parentheses" node (e.g.a * (b + c)is represented by the ASTmultiply(a, add(b, c))instead ofmultiply(a, parentheses(add(b, c)))). Instead, the printer automatically inserts parentheses as necessary to maintain the semantics of the AST, which means all of the optimizations that run over the AST don't have to worry about keeping the parentheses up to date. Similarly, the CSS AST for the dimension token stores the actual unit and the printer makes sure the unit is properly escaped depending on what value it's placed after. All of the other code operating on CSS ASTs doesn't have to worry about parsing escapes to compare units or about keeping escapes up to date when the AST is modified. Hopefully that makes sense. -
Attempt to avoid creating the
node_modules/.cachedirectory for people that use Yarn 2+ in Plug'n'Play mode (#2685)When Yarn's PnP mode is enabled, packages installed by Yarn may or may not be put inside
.zipfiles. The specific heuristics for when this happens change over time in between Yarn versions. This is problematic for esbuild because esbuild's JavaScript package needs to execute a binary file inside the package. Yarn makes extensive modifications to Node's file system APIs at run time to pretend that.zipfiles are normal directories and to make it hard to tell whether a file is real or not (since in theory it doesn't matter). But they haven't modified Node'schild_process.execFileSyncAPI so attempting to execute a file inside a zip file fails. To get around this, esbuild previously used Node's file system APIs to copy the binary executable to another location before invokingexecFileSync. Under the hood this caused Yarn to extract the file from the zip file into a real file that can then be run.However, esbuild copied its executable into
node_modules/.cache/esbuild. This is the official recommendation from the Yarn team for where packages are supposed to put these types of files when Yarn PnP is being used. However, users of Yarn PnP with esbuild find this really annoying because they don't like looking at thenode_modulesdirectory. With this release, esbuild now sets"preferUnplugged": truein itspackage.jsonfiles, which tells newer versions of Yarn to not put esbuild's packages in a zip file. There may exist older versions of Yarn that don't supportpreferUnplugged. In that case esbuild should still copy the executable to a cache directory, so it should still run (hopefully, since I haven't tested this myself). Note that esbuild setting"preferUnplugged": truemay have the side effect of esbuild taking up more space on the file system in the event that multiple platforms are installed simultaneously, or that you're using an older version of Yarn that always installs packages for all platforms. In that case you may want to update to a newer version of Yarn since Yarn has recently changed to only install packages for the current platform.
-
Fix parsing of TypeScript
inferinside a conditionalextends(#2675)Unlike JavaScript, parsing TypeScript sometimes requires backtracking. The
infer Atype operator can take an optional constraint of the forminfer A extends B. However, this syntax conflicts with the similar conditional type operatorA extends B ? C : Din cases where the syntax is combined, such asinfer A extends B ? C : D. This is supposed to be parsed as(infer A) extends B ? C : D. Previously esbuild incorrectly parsed this as(infer A extends B) ? C : Dinstead, which is a parse error since the?:conditional operator requires theextendskeyword as part of the conditional type. TypeScript disambiguates by speculatively parsing theextendsafter theinfer, but backtracking if a?token is encountered afterward. With this release, esbuild should now do the same thing, so esbuild should now correctly parse these types. Here's a real-world example of such a type:type Normalized<T> = T extends Array<infer A extends object ? infer A : never> ? Dictionary<Normalized<A>> : { [P in keyof T]: T[P] extends Array<infer A extends object ? infer A : never> ? Dictionary<Normalized<A>> : Normalized<T[P]> } -
Avoid unnecessary watch mode rebuilds when debug logging is enabled (#2661)
When debug-level logs are enabled (such as with
--log-level=debug), esbuild's path resolution subsystem generates debug log messages that say something like "Read 20 entries for directory /home/user" to help you debug what esbuild's path resolution is doing. This caused esbuild's watch mode subsystem to add a dependency on the full list of entries in that directory since if that changes, the generated log message would also have to be updated. However, meant that on systems where a parent directory undergoes constant directory entry churn, esbuild's watch mode would continue to rebuild if--log-level=debugwas passed.With this release, these debug log messages are now generated by "peeking" at the file system state while bypassing esbuild's watch mode dependency tracking. So now watch mode doesn't consider the count of directory entries in these debug log messages to be a part of the build that needs to be kept up to date when the file system state changes.
-
Add support for the TypeScript 4.9
satisfiesoperator (#2509)TypeScript 4.9 introduces a new operator called
satisfiesthat lets you check that a given value satisfies a less specific type without casting it to that less specific type and without generating any additional code at run-time. It looks like this:const value = { foo: 1, bar: false } satisfies Record<string, number | boolean> console.log(value.foo.toFixed(1)) // TypeScript knows that "foo" is a number hereBefore this existed, you could use a cast with
asto check that a value satisfies a less specific type, but that removes any additional knowledge that TypeScript has about that specific value:const value = { foo: 1, bar: false } as Record<string, number | boolean> console.log(value.foo.toFixed(1)) // TypeScript no longer knows that "foo" is a numberYou can read more about this feature in TypeScript's blog post for 4.9 as well as the associated TypeScript issue for this feature.
This feature was implemented in esbuild by @magic-akari.
-
Fix watch mode constantly rebuilding if the parent directory is inaccessible (#2640)
Android is unusual in that it has an inaccessible directory in the path to the root, which esbuild was not originally built to handle. To handle cases like this, the path resolution layer in esbuild has a hack where it treats inaccessible directories as empty. However, esbuild's watch implementation currently triggers a rebuild if a directory previously encountered an error but the directory now exists. The assumption is that the previous error was caused by the directory not existing. Although that's usually the case, it's not the case for this particular parent directory on Android. Instead the error is that the directory previously existed but was inaccessible.
This discrepancy between esbuild's path resolution layer and its watch mode was causing watch mode to rebuild continuously on Android. With this release, esbuild's watch mode instead checks for an error status change in the
readdirfile system call, so watch mode should no longer rebuild continuously on Android. -
Apply a fix for a rare deadlock with the JavaScript API (#1842, #2485)
There have been reports of esbuild sometimes exiting with an "all goroutines are asleep" deadlock message from the Go language runtime. This issue hasn't made much progress until recently, where a possible cause was discovered (thanks to @jfirebaugh for the investigation). This release contains a possible fix for that possible cause, so this deadlock may have been fixed. The fix cannot be easily verified because the deadlock is non-deterministic and rare. If this was indeed the cause, then this issue only affected the JavaScript API in situations where esbuild was already in the process of exiting.
In detail: The underlying cause is that Go's
sync.WaitGroupAPI for waiting for a set of goroutines to finish is not fully thread-safe. Specifically it's not safe to callAdd()concurrently withWait()when the wait group counter is zero due to a data race. This situation could come up with esbuild's JavaScript API when the host JavaScript process closes the child process's stdin and the child process (with no active tasks) callsWait()to check that there are no active tasks, at the same time as esbuild's watchdog timer callsAdd()to add an active task (that pings the host to see if it's still there). The fix in this release is to avoid callingAdd()once we learn that stdin has been closed but before we callWait().
-
Fix minifier correctness bug with single-use substitutions (#2619)
When minification is enabled, esbuild will attempt to eliminate variables that are only used once in certain cases. For example, esbuild minifies this code:
function getEmailForUser(name) { let users = db.table('users'); let user = users.find({ name }); let email = user?.get('email'); return email; }into this code:
function getEmailForUser(e){return db.table("users").find({name:e})?.get("email")}However, this transformation had a bug where esbuild did not correctly consider the "read" part of binary read-modify-write assignment operators. For example, it's incorrect to minify the following code into
bar += fn()because the call tofn()might modifybar:const foo = fn(); bar += foo;In addition to fixing this correctness bug, this release also improves esbuild's output in the case where all values being skipped over are primitives:
function toneMapLuminance(r, g, b) { let hdr = luminance(r, g, b) let decay = 1 / (1 + hdr) return 1 - decay }Previous releases of esbuild didn't substitute these single-use variables here, but esbuild will now minify this to the following code starting with this release:
function toneMapLuminance(e,n,a){return 1-1/(1+luminance(e,n,a))}
-
Fix various edge cases regarding template tags and
this(#2610)This release fixes some bugs where the value of
thiswasn't correctly preserved when evaluating template tags in a few edge cases. These edge cases are listed below:async function test() { class Foo { foo() { return this } } class Bar extends Foo { a = async () => super.foo`` b = async () => super['foo']`` c = async (foo) => super[foo]`` } function foo() { return this } const obj = { foo } const bar = new Bar console.log( (await bar.a()) === bar, (await bar.b()) === bar, (await bar.c('foo')) === bar, { foo }.foo``.foo === foo, (true && obj.foo)`` !== obj, (false || obj.foo)`` !== obj, (null ?? obj.foo)`` !== obj, ) } test()Each edge case in the code above previously incorrectly printed
falsewhen run through esbuild with--minify --target=es6but now correctly printstrue. These edge cases are unlikely to have affected real-world code.
-
Add support for node's "pattern trailers" syntax (#2569)
After esbuild implemented node's
exportsfeature inpackage.json, node changed the feature to also allow text after*wildcards in patterns. Previously the*was required to be at the end of the pattern. It lets you do something like this:{ "exports": { "./features/*": "./features/*.js", "./features/*.js": "./features/*.js" } }With this release, esbuild now supports these types of patterns too.
-
Fix subpath imports with Yarn PnP (#2545)
Node has a little-used feature called subpath imports which are package-internal imports that start with
#and that go through theimportsmap inpackage.json. Previously esbuild had a bug that caused esbuild to not handle these correctly in packages installed via Yarn's "Plug'n'Play" installation strategy. The problem was that subpath imports were being checked after Yarn PnP instead of before. This release reorders these checks, which should allow subpath imports to work in this case. -
Link from JS to CSS in the metafile (#1861, #2565)
When you import CSS into a bundled JS file, esbuild creates a parallel CSS bundle next to your JS bundle. So if
app.tsimports some CSS files and you bundle it, esbuild will give youapp.jsandapp.css. You would then add both<script src="app.js"></script>and<link href="app.css" rel="stylesheet">to your HTML to include everything in the page. This approach is more efficient than having esbuild insert additional JavaScript intoapp.jsthat downloads and includesapp.cssbecause it means the browser can download and parse both the CSS and the JS in parallel (and potentially apply the CSS before the JS has even finished downloading).However, sometimes it's difficult to generate the
<link>tag. One case is when you've added[hash]to the entry names setting to include a content hash in the file name. Then the file name will look something likeapp-GX7G2SBE.cssand may change across subsequent builds. You can tell esbuild to generate build metadata using themetafileAPI option but the metadata only tells you which generated JS bundle corresponds to a JS entry point (via theentryPointproperty), not which file corresponds to the associated CSS bundle. Working around this was hacky and involved string manipulation.This release adds the
cssBundleproperty to the metafile to make this easier. It's present on the metadata for the generated JS bundle and points to the associated CSS bundle. So to generate the HTML tags for a given JS entry point, you first find the output file with theentryPointyou are looking for (and put that in a<script>tag), then check for thecssBundleproperty to find the associated CSS bundle (and put that in a<link>tag).One thing to note is that there is deliberately no
jsBundleproperty mapping the other way because it's not a 1:1 relationship. Two JS bundles can share the same CSS bundle in the case where the associated CSS bundles have the same name and content. In that case there would be no one value for a hypotheticaljsBundleproperty to have.
-
Fix an obscure npm package installation issue with
--omit=optional(#2558)The previous release introduced a regression with
npm install esbuild --omit=optionalwhere the filenode_modules/.bin/esbuildwould no longer be present after installation. That could cause any package scripts which used theesbuildcommand to no longer work. This release fixes the regression sonode_modules/.bin/esbuildshould now be present again after installation. This regression only affected people installing esbuild usingnpmwith either the--omit=optionalor--no-optionalflag, which is a somewhat unusual situation.More details:
The reason for this regression is due to some obscure npm implementation details. Since the Go compiler doesn't support trivial cross-compiling on certain Android platforms, esbuild's installer installs a WebAssembly shim on those platforms instead. In the previous release I attempted to simplify esbuild's WebAssembly shims to depend on the
esbuild-wasmpackage instead of including another whole copy of the WebAssembly binary (to make publishing faster and to save on file system space after installation). However, both theesbuildpackage and theesbuild-wasmpackage provide a binary calledesbuildand it turns out that addingesbuild-wasmas a nested dependency of theesbuildpackage (specificallyesbuildoptionally depends on@esbuild/android-armwhich depends onesbuild-wasm) caused npm to be confused about whatnode_modules/.bin/esbuildis supposed to be.It's pretty strange and unexpected that disabling the installation of optional dependencies altogether would suddenly cause an optional dependency's dependency to conflict with the top-level package. What happens under the hood is that if
--omit=optionalis present, npm attempts to uninstall theesbuild-wasmnested dependency at the end ofnpm install(even though theesbuild-wasmpackage was never installed due to--omit=optional). This uninstallation causesnode_modules/.bin/esbuildto be deleted.After doing a full investigation, I discovered that npm's handling of the
.bindirectory is deliberately very brittle. When multiple packages in the dependency tree put something in.binwith the same name, the end result is non-deterministic/random. What you get in.binmight be from one package, from the other package, or might be missing entirely. The workaround suggested by npm is to just avoid having two packages that put something in.binwith the same name. So this was fixed by making the@esbuild/android-armandesbuild-android-64packages each include another whole copy of the WebAssembly binary, which works because these packages don't put anything in.bin.
-
Fix JSX name collision edge case (#2534)
Code generated by esbuild could have a name collision in the following edge case:
- The JSX transformation mode is set to
automatic, which causesimportstatements to be inserted - An element uses a
{...spread}followed by akey={...}, which uses the legacycreateElementfallback imported fromreact - Another import uses a name that ends with
reactsuch as@remix-run/react - The output format has been set to CommonJS so that
importstatements are converted into require calls
In this case, esbuild previously generated two variables with the same name
import_react, like this:var import_react = require("react"); var import_react2 = require("@remix-run/react");That bug is fixed in this release. The code generated by esbuild no longer contains a name collision.
- The JSX transformation mode is set to
-
Fall back to WebAssembly on Android ARM (#1556, #1578, #2335, #2526)
Go's compiler supports trivial cross-compiling to almost all platforms without installing any additional software other than the Go compiler itself. This has made it very easy for esbuild to publish native binary executables for many platforms. However, it strangely doesn't support cross-compiling to Android ARM without installing the Android build tools.
So instead of publishing a native esbuild binary executable to npm, this release publishes a WebAssembly fallback build. This is essentially the same as the
esbuild-wasmpackage but it's installed automatically when you install theesbuildpackage on Android ARM. So packages that depend on theesbuildpackage should now work on Android ARM. This change has not yet been tested end-to-end because I don't have a 32-bit Android ARM device myself, but in theory it should work.This inherits the drawbacks of WebAssembly including significantly slower performance than native as well as potentially also more severe memory usage limitations and lack of certain features (e.g.
--serve). If you want to use a native binary executable of esbuild on Android ARM, you may be able to build it yourself from source after installing the Android build tools. -
Attempt to better support Yarn's
ignorePatternDatafeature (#2495)Part of resolving paths in a project using Yarn's Plug'n'Play feature involves evaluating a regular expression in the
ignorePatternDataproperty of.pnp.data.json. However, it turns out that the particular regular expressions generated by Yarn use some syntax that works with JavaScript regular expressions but that does not work with Go regular expressions.In this release, esbuild will now strip some of the the problematic syntax from the regular expression before compiling it, which should hopefully allow it to be compiled by Go's regular expression engine. The specific character sequences that esbuild currently strips are as follows:
(?!\.)(?!(?:^|\/)\.)(?!\.{1,2}(?:\/|$))(?!(?:^|\/)\.{1,2}(?:\/|$))
These seem to be used by Yarn to avoid the
.and..path segments in the middle of relative paths. The removal of these character sequences seems relatively harmless in this case since esbuild shouldn't ever generate such path segments. This change should add support to esbuild for Yarn'spnpIgnorePatternsfeature. -
Fix non-determinism issue with legacy block-level function declarations and strict mode (#2537)
When function declaration statements are nested inside a block in strict mode, they are supposed to only be available within that block's scope. But in "sloppy mode" (which is what non-strict mode is commonly called), they are supposed to be available within the whole function's scope:
// This returns 1 due to strict mode function test1() { 'use strict' function fn() { return 1 } if (true) { function fn() { return 2 } } return fn() } // This returns 2 due to sloppy mode function test2() { function fn() { return 1 } if (true) { function fn() { return 2 } } return fn() }To implement this, esbuild compiles these two functions differently to reflect their different semantics:
function test1() { "use strict"; function fn() { return 1; } if (true) { let fn2 = function() { return 2; }; } return fn(); } function test2() { function fn() { return 1; } if (true) { let fn2 = function() { return 2; }; var fn = fn2; } return fn(); }However, the compilation had a subtle bug where the automatically-generated function-level symbols for multible hoisted block-level function declarations in the same block a sloppy-mode context were generated in a random order if the output was in strict mode, which could be the case if TypeScript's
alwaysStrictsetting was set to true. This lead to non-determinism in the output as the minifier would randomly exchange the generated names for these symbols on different runs. This bug has been fixed by sorting the keys of the unordered map before iterating over them. -
Fix parsing of
@keyframeswith string identifiers (#2555)Firefox supports
@keyframeswith string identifier names. Previously this was treated as a syntax error by esbuild as it doesn't work in any other browser. The specification allows for this however, so it's technically not a syntax error (even though it would be unwise to use this feature at the moment). There was also a bug where esbuild would remove the identifier name in this case as the syntax wasn't recognized.This release changes esbuild's parsing of
@keyframesto now consider this case to be an unrecognized CSS rule. That means it will be passed through unmodified (so you can now use esbuild to bundle this Firefox-specific CSS) but the CSS will not be pretty-printed or minified. I don't think it makes sense for esbuild to have special code to handle this Firefox-specific syntax at this time. This decision can be revisited in the future if other browsers add support for this feature. -
Add the
--jsx-side-effectsAPI option (#2539, #2546)By default esbuild assumes that JSX expressions are side-effect free, which means they are annoated with
/* @__PURE__ */comments and are removed during bundling when they are unused. This follows the common use of JSX for virtual DOM and applies to the vast majority of JSX libraries. However, some people have written JSX libraries that don't have this property. JSX expressions can have arbitrary side effects and can't be removed. If you are using such a library, you can now pass--jsx-side-effectsto tell esbuild that JSX expressions have side effects so it won't remove them when they are unused.This feature was contributed by @rtsao.
-
Add
--watch=foreverto allow esbuild to never terminate (#1511, #1885)Currently using esbuild's watch mode via
--watchfrom the CLI will stop watching if stdin is closed. The rationale is that stdin is automatically closed by the OS when the parent process exits, so stopping watch mode when stdin is closed ensures that esbuild's watch mode doesn't keep running forever after the parent process has been closed. For example, it would be bad if you wrote a shell script that didesbuild --watch &to run esbuild's watch mode in the background, and every time you run the script it creates a newesbuildprocess that runs forever.However, there are cases when it makes sense for esbuild's watch mode to never exit. One such case is within a short-lived VM where the lifetime of all processes inside the VM is expected to be the lifetime of the VM. Previously you could easily do this by piping the output of a long-lived command into esbuild's stdin such as
sleep 999999999 | esbuild --watch &. However, this possibility often doesn't occur to people, and it also doesn't work on Windows. People also sometimes attempt to keep esbuild open by piping an infinite stream of data to esbuild such as withesbuild --watch </dev/zero &which causes esbuild to spin at 100% CPU. So with this release, esbuild now has a--watch=foreverflag that will not stop watch mode when stdin is closed. -
Work around
PATHwithoutnodein install script (#2519)Some people install esbuild's npm package in an environment without the
nodecommand in theirPATH. This fails on Windows because esbuild's install script runs theesbuildcommand before exiting as a sanity check, and on Windows theesbuildcommand has to be a JavaScript file because of some internal details about how npm handles thebinfolder (specifically theesbuildcommand lacks the.exeextension, which is required on Windows). This release attempts to work around this problem by usingprocess.execPathinstead of"node"as the command for running node. In theory this means the installer can now still function on Windows if something is wrong withPATH.
-
Lower
for awaitloops (#1930)This release lowers
for awaitloops to the equivalentforloop containingawaitwhen esbuild is configured such thatfor awaitloops are unsupported. This transform still requires at least generator functions to be supported since esbuild's lowering ofawaitcurrently relies on generators. This new transformation is mostly modeled after what the TypeScript compiler does. Here's an example:async function f() { for await (let x of y) x() }The code above will now become the following code with
--target=es2017(omitting the code for the__forAwaithelper function):async function f() { try { for (var iter = __forAwait(y), more, temp, error; more = !(temp = await iter.next()).done; more = false) { let x = temp.value; x(); } } catch (temp) { error = [temp]; } finally { try { more && (temp = iter.return) && await temp.call(iter); } finally { if (error) throw error[0]; } } } -
Automatically fix invalid
supportedconfigurations (#2497)The
--target=setting lets you tell esbuild to target a specific version of one or more JavaScript runtimes such aschrome80,node14and esbuild will restrict its output to only those features supported by all targeted JavaScript runtimes. More recently, esbuild introduced the--supported:setting that lets you override which features are supported on a per-feature basis. However, this now lets you configure nonsensical things such as--supported:async-await=false --supported:async-generator=true. Previously doing this could result in esbuild building successfully but producing invalid output.Starting with this release, esbuild will now attempt to automatically fix nonsensical feature override configurations by introducing more overrides until the configuration makes sense. So now the configuration from previous example will be changed such that
async-await=falseimpliesasync-generator=false. The full list of implications that were introduced is below:-
async-await=falseimplies:async-generator=falsefor-await=falsetop-level-await=false
-
generator=falseimplies:async-generator=false
-
object-accessors=falseimplies:class-private-accessor=falseclass-private-static-accessor=false
-
class-field=falseimplies:class-private-field=false
-
class-static-field=falseimplies:class-private-static-field=false
-
class=falseimplies:class-field=falseclass-private-accessor=falseclass-private-brand-check=falseclass-private-field=falseclass-private-method=falseclass-private-static-accessor=falseclass-private-static-field=falseclass-private-static-method=falseclass-static-blocks=falseclass-static-field=false
-
-
Implement a small minification improvement (#2496)
Some people write code that contains a label with an immediate break such as
x: break x. Previously this code was not removed during minification but it will now be removed during minification starting with this release. -
Fix installing esbuild via Yarn with
enableScripts: falseconfigured (#2457)If esbuild is installed with Yarn with the
enableScripts: falsesetting configured, then Yarn will not "unplug" theesbuildpackage (i.e. it will keep the entire package inside a.zipfile). This messes with esbuild's library code that extracts the platform-specific binary executable because that code copies the binary executable into the esbuild package directory, and Yarn's.zipfile system shim doesn't let you write to a directory inside of a.zipfile. This release fixes this problem by writing to thenode_modules/.cache/esbuilddirectory instead in this case. So you should now be able to use esbuild with Yarn whenenableScripts: falseis configured.This fix was contributed by @jonaskuske.
-
Fix issues with Yarn PnP and Yarn's workspaces feature (#2476)
This release makes sure esbuild works with a Yarn feature called workspaces. Previously esbuild wasn't tested in this scenario, but this scenario now has test coverage. Getting this to work involved further tweaks to esbuild's custom code for what happens after Yarn PnP's path resolution algorithm runs, which is not currently covered by Yarn's PnP specification. These tweaks also fix
exportsmap resolution with Yarn PnP for non-empty subpaths, which wasn't previously working.
-
Consider TypeScript import assignments to be side-effect free (#2468)
TypeScript has a legacy import syntax for working with TypeScript namespaces that looks like this:
import { someNamespace } from './some-file' import bar = someNamespace.foo; // some-file.ts export namespace someNamespace { export let foo = 123 }Since esbuild converts TypeScript into JavaScript one file at a time, it doesn't know if
baris supposed to be a value or a type (or both, which TypeScript actually allows in this case). This is problematic because values are supposed to be kept during the conversion but types are supposed to be removed during the conversion. Currently esbuild keepsbarin the output, which is done becausesomeNamespace.foois a property access and property accesses run code that could potentially have a side effect (although there is no side effect in this case).With this release, esbuild will now consider
someNamespace.footo have no side effects. This meansbarwill now be removed when bundling and when tree shaking is enabled. Note that it will still not be removed when tree shaking is disabled. This is because in this mode, esbuild supports adding additional code to the end of the generated output that's in the same scope as the module. That code could potentially make use ofbar, so it would be incorrect to remove it. If you wantbarto be removed, you'll have to enable tree shaking (which tells esbuild that nothing else depends on the unexported top-level symbols in the generated output). -
Change the order of the banner and the
"use strict"directive (#2467)Previously the top of the file contained the following things in order:
- The hashbang comment (see below) from the source code, if present
- The
"use strict"directive from the source code, if present - The content of esbuild's
bannerAPI option, if specified
This was problematic for people that used the
bannerAPI option to insert the hashbang comment instead of using esbuild's hashbang comment preservation feature. So with this release, the order has now been changed to:- The hashbang comment (see below) from the source code, if present
- The content of esbuild's
bannerAPI option, if specified - The
"use strict"directive from the source code, if present
I'm considering this change to be a bug fix instead of a breaking change because esbuild's documentation states that the
bannerAPI option can be used to "insert an arbitrary string at the beginning of generated JavaScript files". While this isn't technically true because esbuild may still insert the original hashbang comment before the banner, it's at least more correct now because the banner will now come before the"use strict"directive.For context: JavaScript files recently allowed using a hashbang comment, which starts with
#!and which must start at the very first character of the file. It allows Unix systems to execute the file directly as a script without needing to prefix it by thenodecommand. This comment typically has the value#!/usr/bin/env node. Hashbang comments will be a part of ES2023 when it's released next year. -
Fix
exportsmaps with Yarn PnP path resolution (#2473)The Yarn PnP specification says that to resolve a package path, you first resolve it to the absolute path of a directory, and then you run node's module resolution algorithm on it. Previously esbuild followed this part of the specification. However, doing this means that
exportsinpackage.jsonis not respected because node's module resolution algorithm doesn't interpretexportsfor absolute paths. So with this release, esbuild will now use a modified algorithm that deviates from both specifications but that should hopefully behave more similar to what Yarn actually does: node's module resolution algorithm is run with the original import path but starting from the directory returned by Yarn PnP.
-
Change the Yarn PnP manifest to a singleton (#2463)
Previously esbuild searched for the Yarn PnP manifest in the parent directories of each file. But with Yarn's
enableGlobalCachesetting it's possible to configure Yarn PnP's implementation to reach outside of the directory subtree containing the Yarn PnP manifest. This was causing esbuild to fail to bundle projects with theenableGlobalCachesetting enabled.To handle this case, esbuild will now only search for the Yarn PnP manifest in the current working directory of the esbuild process. If you're using esbuild's CLI, this means you will now have to
cdinto the appropriate directory first. If you're using esbuild's API, you can override esbuild's value for the current working directory with theabsWorkingDirAPI option. -
Fix Yarn PnP resolution failures due to backslashes in paths on Windows (#2462)
Previously dependencies of a Yarn PnP virtual dependency failed to resolve on Windows. This was because Windows uses
\instead of/as a path separator, and the path manipulation algorithms used for Yarn PnP expected/. This release converts\into/in Windows paths, which fixes this issue. -
Fix
sideEffectspatterns containing slashes on Windows (#2465)The
sideEffectsfield inpackage.jsonlets you specify an array of patterns to mark which files have side effects (which causes all other files to be considered to not have side effects by exclusion). That looks like this:"sideEffects": [ "**/index.js", "**/index.prod.js" ]However, the presence of the
/character in the pattern meant that the pattern failed to match Windows-style paths, which brokesideEffectson Windows in this case. This release fixes this problem by adding additional code to handle Windows-style paths.
-
Fix Yarn PnP issue with packages containing
index.js(#2455, #2461)Yarn PnP's tests require the resolved paths to end in
/. That's not how the rest of esbuild's internals work, however, and doing this messed up esbuild's node module path resolution regarding automatically-detectedindex.jsfiles. Previously packages that relied on implicitindex.jsresolution rules didn't work with esbuild under Yarn PnP. Removing this slash has fixed esbuild's path resolution behavior regardingindex.js, which should now the same both with and without Yarn PnP. -
Fix Yarn PnP support for
extendsintsconfig.json(#2456)Previously using
extendsintsconfig.jsonwith a path in a Yarn PnP package didn't work. This is because the process of setting up package path resolution rules requires parsingtsconfig.jsonfiles (due to thebaseUrlandpathsfeatures) and resolvingextendsto a package path requires package path resolution rules to already be set up, which is a circular dependency. This cycle is broken by using special rules forextendsintsconfig.jsonthat bypasses esbuild's normal package path resolution process. This is why usingextendswith a Yarn PnP package didn't automatically work. With this release, these special rules have been modified to check for a Yarn PnP manifest so this case should work now. -
Fix Yarn PnP support in
esbuild-wasm(#2458)When running esbuild via WebAssembly, Yarn PnP support previously failed because Go's file system internals return
EINVALwhen trying to read a.zipfile as a directory when run with WebAssembly. This was unexpected because Go's file system internals returnENOTDIRfor this case on native. This release updates esbuild to treatEINVALlikeENOTDIRin this case, which fixes usingesbuild-wasmto bundle a Yarn PnP project.Note that to be able to use
esbuild-wasmfor Yarn PnP successfully, you currently have to run it usingnodeinstead ofyarn node. This is because the file system shim that Yarn overwrites node's native file system API with currently generates invalid file descriptors with negative values when inside a.zipfile. This prevents esbuild from working correctly because Go's file system internals don't expect syscalls that succeed without an error to return an invalid file descriptor. Yarn is working on fixing their use of invalid file descriptors.
-
Update esbuild's Yarn Plug'n'Play implementation to match the latest specification changes (#2452, #2453)
This release updates esbuild's implementation of Yarn Plug'n'Play to match some changes to Yarn's specification that just landed. The changes are as follows:
-
Check for platform-specific absolute paths instead of always for the
/prefixThe specification previously said that Yarn Plug'n'Play path resolution rules should not apply for paths that start with
/. The intent was to avoid accidentally processing absolute paths. However, absolute paths on Windows such asC:\projectstart with drive letters instead of with/. So the specification was changed to instead explicitly avoid processing absolute paths. -
Make
$$virtualan alias for__virtual__Supporting Yarn-style path resolution requires implementing a custom Yarn-specific path traversal scheme where certain path segments are considered no-ops. Specifically any path containing segments of the form
__virtual__/<whatever>/<n>where<n>is an integer must be treated as if they werentimes the..operator instead (the<whatever>path segment is ignored). So/path/to/project/__virtual__/xyz/2/foo.jsmaps to the underlying file/path/to/project/../../foo.js. This scheme makes it possible for Yarn to get node (and esbuild) to load the same file multiple times (which is sometimes required for correctness) without actually duplicating the file on the file system.However, old versions of Yarn used to use
$$virtualinstead of__virtual__. This was changed because$$virtualwas error-prone due to the use of the$character, which can cause bugs when it's not correctly escaped within regular expressions. Now that esbuild makes$$virtualan alias for__virtual__, esbuild should now work with manifests from these old Yarn versions. -
Ignore PnP manifests in virtual directories
The specification describes the algorithm for how to find the Plug'n'Play manifest when starting from a certain point in the file system: search through all parent directories in reverse order until the manifest is found. However, this interacts poorly with virtual paths since it can end up finding a virtual copy of the manifest instead of the original. To avoid this, esbuild now ignores manifests in virtual directories so that the search for the manifest will continue and find the original manifest in another parent directory later on.
These fixes mean that esbuild's implementation of Plug'n'Play now matches Yarn's implementation more closely, and esbuild can now correctly build more projects that use Plug'n'Play.
-
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.14.0. See the documentation about semver for more information.
-
Implement the Yarn Plug'n'Play module resolution algorithm (#154, #237, #1263, #2451)
Node comes with a package manager called npm, which installs packages into a
node_modulesfolder. Node and esbuild both come with built-in rules for resolving import paths to packages withinnode_modules, so packages installed via npm work automatically without any configuration. However, many people use an alternative package manager called Yarn. While Yarn can install packages usingnode_modules, it also offers a different package installation strategy called Plug'n'Play, which is often shortened to "PnP" (not to be confused with pnpm, which is an entirely different unrelated package manager).Plug'n'Play installs packages as
.zipfiles on your file system. The packages are never actually unzipped. Since Node doesn't know anything about Yarn's package installation strategy, this means you can no longer run your code with Node as it won't be able to find your packages. Instead, you need to run your code with Yarn, which applies patches to Node's file system APIs before running your code. These patches attempt to make zip files seem like normal directories. When running under Yarn, using Node's file system API to read./some.zip/lib/file.jsactually automatically extractslib/file.jsfrom./some.zipat run-time as if it was a normal file. Other file system APIs behave similarly. However, these patches don't work with esbuild because esbuild is not written in JavaScript; it's a native binary executable that interacts with the file system directly through the operating system.Previously the workaround for using esbuild with Plug'n'Play was to use the
@yarnpkg/esbuild-plugin-pnpplugin with esbuild's JavaScript API. However, this wasn't great because the plugin needed to potentially intercept every single import path and file load to check whether it was a Plug'n'Play package, which has an unusually high performance cost. It also meant that certain subtleties of path resolution rules within a.zipfile could differ slightly from the way esbuild normally works since path resolution inside.zipfiles was implemented by Yarn, not by esbuild (which is due to a limitation of esbuild's plugin API).With this release, esbuild now contains an independent implementation of Yarn's Plug'n'Play algorithm (which is used when esbuild finds a
.pnp.js,.pnp.cjs, or.pnp.data.jsonfile in the directory tree). Creating additional implementations of this algorithm recently became possible because Yarn's package manifest format was recently documented: https://yarnpkg.com/advanced/pnp-spec/. This should mean that you can now use esbuild to bundle Plug'n'Play projects without any additional configuration (so you shouldn't need@yarnpkg/esbuild-plugin-pnpanymore). Bundling these projects should now happen much faster as Yarn no longer even needs to be run at all. Bundling the Yarn codebase itself with esbuild before and after this change seems to demonstrate over a 10x speedup (3.4s to 0.24s). And path resolution rules within Yarn packages should now be consistent with how esbuild handles regular Node packages. For example, fields such asmoduleandbrowserinpackage.jsonfiles within.zipfiles should now be respected.Keep in mind that this is brand new code and there may be some initial issues to work through before esbuild's implementation is solid. Yarn's Plug'n'Play specification is also brand new and may need some follow-up edits to guide new implementations to match Yarn's exact behavior. If you try this out, make sure to test it before committing to using it, and let me know if anything isn't working as expected. Should you need to debug esbuild's path resolution, you may find
--log-level=verbosehelpful.
-
Fix optimizations for calls containing spread arguments (#2445)
This release fixes the handling of spread arguments in the optimization of
/* @__PURE__ */comments, empty functions, and identity functions:// Original code function empty() {} function identity(x) { return x } /* @__PURE__ */ a(...x) /* @__PURE__ */ new b(...x) empty(...x) identity(...x) // Old output (with --minify --tree-shaking=true) ...x;...x;...x;...x; // New output (with --minify --tree-shaking=true) function identity(n){return n}[...x];[...x];[...x];identity(...x);Previously esbuild assumed arguments with side effects could be directly inlined. This is almost always true except for spread arguments, which are not syntactically valid on their own and which have the side effect of causing iteration, which might have further side effects. Now esbuild will wrap these elements in an unused array so that they are syntactically valid and so that the iteration side effects are preserved.
This release fixes a minor issue with the previous release: I had to rename the package esbuild-linux-loong64 to @esbuild/linux-loong64 in the contributed PR because someone registered the package name before I could claim it, and I missed a spot. Hopefully everything is working after this release. I plan to change all platform-specific package names to use the @esbuild/ scope at some point to avoid this problem in the future.
-
Allow binary data as input to the JS
transformandbuildAPIs (#2424)Previously esbuild's
transformandbuildAPIs could only take a string. However, some people want to use esbuild to convert binary data to base64 text. This is problematic because JavaScript strings represent UTF-16 text and esbuild internally operates on arrays of bytes, so all strings coming from JavaScript undergo UTF-16 to UTF-8 conversion before use. This meant that using esbuild in this way was doing base64 encoding of the UTF-8 encoding of the text, which was undesired.With this release, esbuild now accepts
Uint8Arrayin addition to string as an input format for thetransformandbuildAPIs. Now you can use esbuild to convert binary data to base64 text:// Original code import esbuild from 'esbuild' console.log([ (await esbuild.transform('\xFF', { loader: 'base64' })).code, (await esbuild.build({ stdin: { contents: '\xFF', loader: 'base64' }, write: false })).outputFiles[0].text, ]) console.log([ (await esbuild.transform(new Uint8Array([0xFF]), { loader: 'base64' })).code, (await esbuild.build({ stdin: { contents: new Uint8Array([0xFF]), loader: 'base64' }, write: false })).outputFiles[0].text, ]) // Old output [ 'module.exports = "w78=";\n', 'module.exports = "w78=";\n' ] /* ERROR: The input to "transform" must be a string */ // New output [ 'module.exports = "w78=";\n', 'module.exports = "w78=";\n' ] [ 'module.exports = "/w==";\n', 'module.exports = "/w==";\n' ] -
Update the getter for
textin build results (#2423)Output files in build results returned from esbuild's JavaScript API have both a
contentsand atextproperty to return the contents of the output file. Thecontentsproperty is a binary UTF-8 Uint8Array and thetextproperty is a JavaScript UTF-16 string. Thetextproperty is a getter that does the UTF-8 to UTF-16 conversion only if it's needed for better performance.Previously if you mutate the build results object, you had to overwrite both
contentsandtextsince the value returned from thetextgetter is the original text returned by esbuild. Some people find this confusing so with this release, the getter fortexthas been updated to do the UTF-8 to UTF-16 conversion on the current value of thecontentsproperty instead of the original value. -
Publish builds for Linux LoongArch 64-bit (#1804, #2373)
This release upgrades to Go 1.19, which now includes support for LoongArch 64-bit processors. LoongArch 64-bit builds of esbuild will now be published to npm, which means that in theory they can now be installed with
npm install esbuild. This was contributed by @beyond-1234.
-
Add support for React 17's
automaticJSX transform (#334, #718, #1172, #2318, #2349)This adds support for the new "automatic" JSX runtime from React 17+ to esbuild for both the build and transform APIs.
New CLI flags and API options:
--jsx,jsx— Set this to"automatic"to opt in to this new transform--jsx-dev,jsxDev— Toggles development mode for the automatic runtime--jsx-import-source,jsxImportSource— Overrides the root import for runtime functions (default"react")
New JSX pragma comments:
@jsxRuntime— Sets the runtime (automaticorclassic)@jsxImportSource— Sets the import source (only valid with automatic runtime)
The existing
@jsxFragmentand@jsxFactorypragma comments are only valid with "classic" runtime.TSConfig resolving: Along with accepting the new options directly via CLI or API, option inference from
tsconfig.jsoncompiler options was also implemented:"jsx": "preserve"or"jsx": "react-native"→ Same as--jsx=preservein esbuild"jsx": "react"→ Same as--jsx=transformin esbuild (which is the default behavior)"jsx": "react-jsx"→ Same as--jsx=automaticin esbuild"jsx": "react-jsxdev"→ Same as--jsx=automatic --jsx-devin esbuild
It also reads the value of
"jsxImportSource"fromtsconfig.jsonif specified.For
react-jsxit's important to note that it doesn't implicitly disable--jsx-dev. This is to support the case where a user sets"react-jsx"in theirtsconfig.jsonbut then toggles development mode directly in esbuild.esbuild vs Babel vs TS vs...
There are a few differences between the various technologies that implement automatic JSX runtimes. The JSX transform in esbuild follows a mix of Babel's and TypeScript's behavior:
-
When an element has
__sourceor__selfprops:- Babel: Print an error about a deprecated transform plugin
- TypeScript: Allow the props
- swc: Hard crash
- esbuild: Print an error — Following Babel was chosen for this one because this might help people catch configuration issues where JSX files are being parsed by multiple tools
-
Element has an "implicit true" key prop, e.g.
<a key />:- Babel: Print an error indicating that "key" props require an explicit value
- TypeScript: Silently omit the "key" prop
- swc: Hard crash
- esbuild: Print an error like Babel — This might help catch legitimate programming mistakes
-
Element has spread children, e.g.
<a>{...children}</a>- Babel: Print an error stating that React doesn't support spread children
- TypeScript: Use static jsx function and pass children as-is, including spread operator
- swc: same as Babel
- esbuild: Same as TypeScript
Also note that TypeScript has some bugs regarding JSX development mode and the generation of
lineNumberandcolumnNumbervalues. Babel's values are accurate though, so esbuild's line and column numbers match Babel. Both numbers are 1-based and columns are counted in terms of UTF-16 code units.This feature was contributed by @jgoz.
-
Emit
namesin source maps (#1296)The source map specification includes an optional
namesfield that can associate an identifier with a mapping entry. This can be used to record the original name for an identifier, which is useful if the identifier was renamed to something else in the generated code. When esbuild was originally written, this field wasn't widely used, but now there are some debuggers that make use of it to provide better debugging of minified code. With this release, esbuild now includes anamesfield in the source maps that it generates. To save space, the original name is only recorded when it's different from the final name. -
Update parser for arrow functions with initial default type parameters in
.tsxfiles (#2410)TypeScript 4.6 introduced a change to the parsing of JSX syntax in
.tsxfiles. Now a<token followed by an identifier and then a=token is parsed as an arrow function with a default type parameter instead of as a JSX element. This release updates esbuild's parser to match TypeScript's parser. -
Fix an accidental infinite loop with
--definesubstitution (#2407)This is a fix for a regression that was introduced in esbuild version 0.14.44 where certain
--definesubstitutions could result in esbuild crashing with a stack overflow. The problem was an incorrect fix for #2292. The fix merged the code paths for--defineand--jsx-factoryrewriting since the value substitution is now the same for both. However, doing this accidentally made--definesubstitution recursive since the JSX factory needs to be able to match against--definesubstitutions to integrate with the--injectfeature. The fix is to only do one additional level of matching against define substitutions, and to only do this for JSX factories. Now these cases are able to build successfully without a stack overflow. -
Include the "public path" value in hashes (#2403)
The
--public-path=configuration value affects the paths that esbuild uses to reference files from other files and is used in various situations such as cross-chunk imports in JS and references to asset files from CSS files. However, it wasn't included in the hash calculations used for file names due to an oversight. This meant that changing the public path setting incorrectly didn't result in the hashes in file names changing even though the contents of the files changed. This release fixes the issue by including a hash of the public path in all non-asset output files. -
Fix a cross-platform consistency bug (#2383)
Previously esbuild would minify
0xFFFF_FFFF_FFFF_FFFFas0xffffffffffffffff(18 bytes) on arm64 chips and as18446744073709552e3(19 bytes) on x86_64 chips. The reason was that the number was converted to a 64-bit unsigned integer internally for printing as hexadecimal, the 64-bit floating-point number0xFFFF_FFFF_FFFF_FFFFis actually0x1_0000_0000_0000_0180(i.e. it's rounded up, not down), and convertingfloat64touint64is implementation-dependent in Go when the input is out of bounds. This was fixed by changing the upper limit for which esbuild uses hexadecimal numbers during minification to0xFFFF_FFFF_FFFF_F800, which is the next representable 64-bit floating-point number below0x1_0000_0000_0000_0180, and which fits in auint64. As a result, esbuild will now consistently never minify0xFFFF_FFFF_FFFF_FFFFas0xffffffffffffffffanymore, which means the output should now be consistent across platforms. -
Fix a hang with the synchronous API when the package is corrupted (#2396)
An error message is already thrown when the esbuild package is corrupted and esbuild can't be run. However, if you are using a synchronous call in the JavaScript API in worker mode, esbuild will use a child worker to initialize esbuild once so that the overhead of initializing esbuild can be amortized across multiple synchronous API calls. However, errors thrown during initialization weren't being propagated correctly which resulted in a hang while the main thread waited forever for the child worker to finish initializing. With this release, initialization errors are now propagated correctly so calling a synchronous API call when the package is corrupted should now result in an error instead of a hang.
-
Fix
tsconfig.jsonfiles that collide with directory names (#2411)TypeScript lets you write
tsconfig.jsonfiles withextendsclauses that refer to another config file using an implicit.jsonfile extension. However, if the config file without the.jsonextension existed as a directory name, esbuild and TypeScript had different behavior. TypeScript ignores the directory and continues looking for the config file by adding the.jsonextension while esbuild previously terminated the search and then failed to load the config file (because it's a directory). With this release, esbuild will now ignore exact matches when resolvingextendsfields intsconfig.jsonfiles if the exact match results in a directory. -
Add
platformto the transform API (#2362)The
platformoption is mainly relevant for bundling because it mostly affects path resolution (e.g. activating the"browser"field inpackage.jsonfiles), so it was previously only available for the build API. With this release, it has additionally be made available for the transform API for a single reason: you can now set--platform=nodewhen transforming a string so that esbuild will add export annotations for node, which is only relevant when--format=cjsis also present.This has to do with an implementation detail of node that parses the AST of CommonJS files to discover named exports when importing CommonJS from ESM. However, this new addition to esbuild's API is of questionable usefulness. Node's loader API (the main use case for using esbuild's transform API like this) actually bypasses the content returned from the loader and parses the AST that's present on the file system, so you won't actually be able to use esbuild's API for this. See the linked issue for more information.
-
Keep inlined constants when direct
evalis present (#2361)Version 0.14.19 of esbuild added inlining of certain
constvariables during minification, which replaces all references to the variable with the initializer and then removes the variable declaration. However, this could generate incorrect code when directevalis present because the directevalcould reference the constant by name. This release fixes the problem by preserving theconstvariable declaration in this case:// Original code console.log((() => { const x = 123; return x + eval('x') })) // Old output (with --minify) console.log(()=>123+eval("x")); // New output (with --minify) console.log(()=>{const x=123;return 123+eval("x")}); -
Fix an incorrect error in TypeScript when targeting ES5 (#2375)
Previously when compiling TypeScript code to ES5, esbuild could incorrectly consider the following syntax forms as a transformation error:
0 ? ([]) : 1 ? ({}) : 2;The error messages looked like this:
✘ [ERROR] Transforming destructuring to the configured target environment ("es5") is not supported yet example.ts:1:5: 1 │ 0 ? ([]) : 1 ? ({}) : 2; ╵ ^ ✘ [ERROR] Transforming destructuring to the configured target environment ("es5") is not supported yet example.ts:1:16: 1 │ 0 ? ([]) : 1 ? ({}) : 2; ╵ ^These parenthesized literals followed by a colon look like the start of an arrow function expression followed by a TypeScript return type (e.g.
([]) : 1could be the start of the TypeScript arrow function([]): 1 => 1). Unlike in JavaScript, parsing arrow functions in TypeScript requires backtracking. In this case esbuild correctly determined that this expression wasn't an arrow function after all but the check for destructuring was incorrectly not covered under the backtracking process. With this release, the error message is now only reported if the parser successfully parses an arrow function without backtracking. -
Fix generated TypeScript
enumcomments containing*/(#2369, #2371)TypeScript
enumvalues that are equal to a number or string literal are inlined (references to the enum are replaced with the literal value) and have a/* ... */comment after them with the original enum name to improve readability. However, this comment is omitted if the enum name contains the character sequence*/because that would end the comment early and cause a syntax error:// Original TypeScript enum Foo { '/*' = 1, '*/' = 2 } console.log(Foo['/*'], Foo['*/']) // Generated JavaScript console.log(1 /* /* */, 2);This was originally handled correctly when TypeScript
enuminlining was initially implemented since it was only supported within a single file. However, when esbuild was later extended to support TypeScriptenuminlining across files, this special case where the enum name contains*/was not handled in that new code. Starting with this release, esbuild will now handle enums with names containing*/correctly when they are inlined across files:// foo.ts export enum Foo { '/*' = 1, '*/' = 2 } // bar.ts import { Foo } from './foo' console.log(Foo['/*'], Foo['*/']) // Old output (with --bundle --format=esm) console.log(1 /* /* */, 2 /* */ */); // New output (with --bundle --format=esm) console.log(1 /* /* */, 2);This fix was contributed by @magic-akari.
-
Allow
declareclass fields to be initialized (#2380)This release fixes an oversight in the TypeScript parser that disallowed initializers for
declareclass fields. TypeScript actually allows the following limited initializer expressions forreadonlyfields:declare const enum a { b = 0 } class Foo { // These are allowed by TypeScript declare readonly a = 0 declare readonly b = -0 declare readonly c = 0n declare readonly d = -0n declare readonly e = 'x' declare readonly f = `x` declare readonly g = a.b declare readonly h = a['b'] // These are not allowed by TypeScript declare readonly x = (0) declare readonly y = null declare readonly z = -a.b }So with this release, esbuild now allows initializers for
declareclass fields too. To future-proof this in case TypeScript allows more expressions as initializers in the future (such asnull), esbuild will allow any expression as an initializer and will leave the specifics of TypeScript's special-casing here to the TypeScript type checker. -
Fix a bug in esbuild's feature compatibility table generator (#2365)
Passing specific JavaScript engines to esbuild's
--targetflag restricts esbuild to only using JavaScript features that are supported on those engines in the output files that esbuild generates. The data for this feature is automatically derived from this compatibility table with a script: https://kangax.github.io/compat-table/.However, the script had a bug that could incorrectly consider a JavaScript syntax feature to be supported in a given engine even when it doesn't actually work in that engine. Specifically this bug happened when a certain aspect of JavaScript syntax has always worked incorrectly in that engine and the bug in that engine has never been fixed. This situation hasn't really come up before because previously esbuild pretty much only targeted JavaScript engines that always fix their bugs, but the two new JavaScript engines that were added in the previous release (Hermes and Rhino) have many aspects of the JavaScript specification that have never been implemented, and may never be implemented. For example, the
letandconstkeywords are not implemented correctly in those engines.With this release, esbuild's compatibility table generator script has been fixed and as a result, esbuild will now correctly consider a JavaScript syntax feature to be unsupported in a given engine if there is some aspect of that syntax that is broken in all known versions of that engine. This means that the following JavaScript syntax features are no longer considered to be supported by these engines (represented using esbuild's internal names for these syntax features):
Hermes:
arrowconst-and-letdefault-argumentgeneratoroptional-catch-bindingoptional-chainrest-argumenttemplate-literal
Rhino:
arrowconst-and-letdestructuringfor-ofgeneratorobject-extensionstemplate-literal
IE:
const-and-let
-
Enable using esbuild in Deno via WebAssembly (#2323)
The native implementation of esbuild is much faster than the WebAssembly version, but some people don't want to give Deno the
--allow-runpermission necessary to run esbuild and are ok waiting longer for their builds to finish when using the WebAssembly backend. With this release, you can now use esbuild via WebAssembly in Deno. To do this you will need to import fromwasm.jsinstead ofmod.js:import * as esbuild from 'https://deno.land/x/esbuild@v0.14.48/wasm.js' const ts = 'let test: boolean = true' const result = await esbuild.transform(ts, { loader: 'ts' }) console.log('result:', result)Make sure you run Deno with
--allow-netso esbuild can download the WebAssembly module. Using esbuild like this starts up a worker thread that runs esbuild in parallel (unless you callesbuild.initialize({ worker: false })to tell esbuild to run on the main thread). If you want to, you can callesbuild.stop()to terminate the worker if you won't be using esbuild anymore and you want to reclaim the memory.Note that Deno appears to have a bug where background WebAssembly optimization can prevent the process from exiting for many seconds. If you are trying to use Deno and WebAssembly to run esbuild quickly, you may need to manually call
Deno.exit(0)after your code has finished running. -
Add support for font file MIME types (#2337)
This release adds support for font file MIME types to esbuild, which means they are now recognized by the built-in local web server and they are now used when a font file is loaded using the
dataurlloader. The full set of newly-added file extension MIME type mappings is as follows:.eot=>application/vnd.ms-fontobject.otf=>font/otf.sfnt=>font/sfnt.ttf=>font/ttf.woff=>font/woff.woff2=>font/woff2
-
Remove
"use strict";when targeting ESM (#2347)All ES module code is automatically in strict mode, so a
"use strict";directive is unnecessary. With this release, esbuild will now remove the"use strict";directive if the output format is ESM. This change makes the generated output file a few bytes smaller:// Original code 'use strict' export let foo = 123 // Old output (with --format=esm --minify) "use strict";let t=123;export{t as foo}; // New output (with --format=esm --minify) let t=123;export{t as foo}; -
Attempt to have esbuild work with Deno on FreeBSD (#2356)
Deno doesn't support FreeBSD, but it's possible to build Deno for FreeBSD with some additional patches on top. This release of esbuild changes esbuild's Deno installer to download esbuild's FreeBSD binary in this situation. This configuration is unsupported although in theory everything should work.
-
Add some more target JavaScript engines (#2357)
This release adds the Rhino and Hermes JavaScript engines to the set of engine identifiers that can be passed to the
--targetflag. You can use this to restrict esbuild to only using JavaScript features that are supported on those engines in the output files that esbuild generates.
-
Make global names more compact when
||=is available (#2331)With this release, the code esbuild generates for the
--global-name=setting is now slightly shorter when you don't configure esbuild such that the||=operator is unsupported (e.g. with--target=chrome80or--supported:logical-assignment=false):// Original code exports.foo = 123 // Old output (with --format=iife --global-name=foo.bar.baz --minify) var foo=foo||{};foo.bar=foo.bar||{};foo.bar.baz=(()=>{var b=(a,o)=>()=>(o||a((o={exports:{}}).exports,o),o.exports);var c=b(f=>{f.foo=123});return c();})(); // New output (with --format=iife --global-name=foo.bar.baz --minify) var foo;((foo||={}).bar||={}).baz=(()=>{var b=(a,o)=>()=>(o||a((o={exports:{}}).exports,o),o.exports);var c=b(f=>{f.foo=123});return c();})(); -
Fix
--mangle-quoted=falsewith--minify-syntax=trueIf property mangling is active and
--mangle-quotedis disabled, quoted properties are supposed to be preserved. However, there was a case when this didn't happen if--minify-syntaxwas enabled, since that internally transformsx['y']intox.yto reduce code size. This issue has been fixed:// Original code x.foo = x['bar'] = { foo: y, 'bar': z } // Old output (with --mangle-props=. --mangle-quoted=false --minify-syntax=true) x.a = x.b = { a: y, bar: z }; // New output (with --mangle-props=. --mangle-quoted=false --minify-syntax=true) x.a = x.bar = { a: y, bar: z };Notice how the property
foois always used unquoted but the propertybaris always used quoted, sofooshould be consistently mangled whilebarshould be consistently not mangled. -
Fix a minification bug regarding
thisand property initializersWhen minification is enabled, esbuild attempts to inline the initializers of variables that have only been used once into the start of the following expression to reduce code size. However, there was a bug where this transformation could change the value of
thiswhen the initializer is a property access and the start of the following expression is a call expression. This release fixes the bug:// Original code function foo(obj) { let fn = obj.prop; fn(); } // Old output (with --minify) function foo(f){f.prop()} // New output (with --minify) function foo(o){let f=o.prop;f()}
-
Add the ability to override support for individual syntax features (#2060, #2290, #2308)
The
targetsetting already lets you configure esbuild to restrict its output by only making use of syntax features that are known to be supported in the configured target environment. For example, settingtargettochrome50causes esbuild to automatically transform optional chain expressions into the equivalent older JavaScript and prevents you from using BigInts, among many other things. However, sometimes you may want to customize this set of unsupported syntax features at the individual feature level.Some examples of why you might want to do this:
-
JavaScript runtimes often do a quick implementation of newer syntax features that is slower than the equivalent older JavaScript, and you can get a speedup by telling esbuild to pretend this syntax feature isn't supported. For example, V8 has a long-standing performance bug regarding object spread that can be avoided by manually copying properties instead of using object spread syntax. Right now esbuild hard-codes this optimization if you set
targetto a V8-based runtime. -
There are many less-used JavaScript runtimes in addition to the ones present in browsers, and these runtimes sometimes just decide not to implement parts of the specification, which might make sense for runtimes intended for embedded environments. For example, the developers behind Facebook's JavaScript runtime Hermes have decided to not implement classes despite it being a major JavaScript feature that was added seven years ago and that is used in virtually every large JavaScript project.
-
You may be processing esbuild's output with another tool, and you may want esbuild to transform certain features and the other tool to transform certain other features. For example, if you are using esbuild to transform files individually to ES5 but you are then feeding the output into Webpack for bundling, you may want to preserve
import()expressions even though they are a syntax error in ES5.
With this release, you can now use
--supported:feature=falseto forcefeatureto be unsupported. This will cause esbuild to either rewrite code that uses the feature into older code that doesn't use the feature (if esbuild is able to), or to emit a build error (if esbuild is unable to). For example, you can use--supported:arrow=falseto turn arrow functions into function expressions and--supported:bigint=falseto make it an error to use a BigInt literal. You can also use--supported:feature=trueto force it to be supported, which means esbuild will pass it through without transforming it. Keep in mind that this is an advanced feature. For most use cases you will probably want to just usetargetinstead of using this.The full set of currently-allowed features are as follows:
JavaScript:
arbitrary-module-namespace-namesarray-spreadarrowasync-awaitasync-generatorbigintclassclass-fieldclass-private-accessorclass-private-brand-checkclass-private-fieldclass-private-methodclass-private-static-accessorclass-private-static-fieldclass-private-static-methodclass-static-blocksclass-static-fieldconst-and-letdefault-argumentdestructuringdynamic-importexponent-operatorexport-star-asfor-awaitfor-ofgeneratorhashbangimport-assertionsimport-metalogical-assignmentnested-rest-bindingnew-targetnode-colon-prefix-importnode-colon-prefix-requirenullish-coalescingobject-accessorsobject-extensionsobject-rest-spreadoptional-catch-bindingoptional-chainregexp-dot-all-flagregexp-lookbehind-assertionsregexp-match-indicesregexp-named-capture-groupsregexp-sticky-and-unicode-flagsregexp-unicode-property-escapesrest-argumenttemplate-literaltop-level-awaittypeof-exotic-object-is-objectunicode-escapes
CSS:
hex-rgbarebecca-purplemodern-rgb-hslinset-propertynesting
Since you can now specify
--supported:object-rest-spread=falseyourself to work around the V8 performance issue mentioned above, esbuild will no longer automatically transform all instances of object spread when targeting a V8-based JavaScript runtime going forward.Note that JavaScript feature transformation is very complex and allowing full customization of the set of supported syntax features could cause bugs in esbuild due to new interactions between multiple features that were never possible before. Consider this to be an experimental feature.
-
-
Implement
extendsconstraints oninfertype variables (#2330)TypeScript 4.7 introduced the ability to write an
extendsconstraint after aninfertype variable, which looks like this:type FirstIfString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;You can read the blog post for more details: https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#extends-constraints-on-infer-type-variables. Previously this was a syntax error in esbuild but with this release, esbuild can now parse this syntax correctly.
-
Allow
defineto match optional chain expressions (#2324)Previously esbuild's
definefeature only matched member expressions that did not use optional chaining. With this release, esbuild will now also match those that use optional chaining:// Original code console.log(a.b, a?.b) // Old output (with --define:a.b=c) console.log(c, a?.b); // New output (with --define:a.b=c) console.log(c, c);This is for compatibility with Webpack's
DefinePlugin, which behaves the same way.
-
Add a log message for ambiguous re-exports (#2322)
In JavaScript, you can re-export symbols from another file using
export * from './another-file'. When you do this from multiple files that export different symbols with the same name, this creates an ambiguous export which is causes that name to not be exported. This is harmless if you don't plan on using the ambiguous export name, so esbuild doesn't have a warning for this. But if you do want a warning for this (or if you want to make it an error), you can now opt-in to seeing this log message with--log-override:ambiguous-reexport=warningor--log-override:ambiguous-reexport=error. The log message looks like this:▲ [WARNING] Re-export of "common" in "example.js" is ambiguous and has been removed [ambiguous-reexport] One definition of "common" comes from "a.js" here: a.js:2:11: 2 │ export let common = 2 ╵ ~~~~~~ Another definition of "common" comes from "b.js" here: b.js:3:14: 3 │ export { b as common } ╵ ~~~~~~ -
Optimize the output of the JSON loader (#2161)
The
jsonloader (which is enabled by default for.jsonfiles) parses the file as JSON and generates a JavaScript file with the parsed expression as thedefaultexport. This behavior is standard and works in both node and the browser (well, as long as you use an import assertion). As an extension, esbuild also allows you to import additional top-level properties of the JSON object directly as a named export. This is beneficial for tree shaking. For example:import { version } from 'esbuild/package.json' console.log(version)If you bundle the above code with esbuild, you'll get something like the following:
// node_modules/esbuild/package.json var version = "0.14.44"; // example.js console.log(version);Most of the
package.jsonfile is irrelevant and has been omitted from the output due to tree shaking. The way esbuild implements this is to have the JavaScript file that's generated from the JSON look something like this with a separate exported variable for each property on the top-level object:// node_modules/esbuild/package.json export var name = "esbuild"; export var version = "0.14.44"; export var repository = "https://github.com/evanw/esbuild"; export var bin = { esbuild: "bin/esbuild" }; ... export default { name, version, repository, bin, ... };However, this means that if you import the
defaultexport instead of a named export, you will get non-optimal output. Thedefaultexport references all top-level properties, leading to many unnecessary variables in the output. With this release esbuild will now optimize this case to only generate additional variables for top-level object properties that are actually imported:// Original code import all, { bar } from 'data:application/json,{"foo":[1,2,3],"bar":[4,5,6]}' console.log(all, bar) // Old output (with --bundle --minify --format=esm) var a=[1,2,3],l=[4,5,6],r={foo:a,bar:l};console.log(r,l); // New output (with --bundle --minify --format=esm) var l=[4,5,6],r={foo:[1,2,3],bar:l};console.log(r,l);Notice how there is no longer an unnecessary generated variable for
foosince it's never imported. And if you only import thedefaultexport, esbuild will now reproduce the original JSON object in the output with all top-level properties compactly inline. -
Add
idto warnings returned from the APIWith this release, warnings returned from esbuild's API now have an
idproperty. This identifies which kind of log message it is, which can be used to more easily filter out certain warnings. For example, reassigning aconstvariable will generate a message with anidof"assign-to-constant". This also gives you the identifier you need to apply a log override for that kind of message: https://esbuild.github.io/api/#log-override.
-
Add a
copyloader (#2255)You can configure the "loader" for a specific file extension in esbuild, which is a way of telling esbuild how it should treat that file. For example, the
textloader means the file is imported as a string while thebinaryloader means the file is imported as aUint8Array. If you want the imported file to stay a separate file, the only option was previously thefileloader (which is intended to be similar to Webpack'sfile-loaderpackage). This loader copies the file to the output directory and imports the path to that output file as a string. This is useful for a web application because you can refer to resources such as.pngimages by importing them for their URL. However, it's not helpful if you need the imported file to stay a separate file but to still behave the way it normally would when the code is run without bundling.With this release, there is now a new loader called
copythat copies the loaded file to the output directory and then rewrites the path of the import statement orrequire()call to point to the copied file instead of the original file. This will automatically add a content hash to the output name by default (which can be configured with the--asset-names=setting). You can use this by specifyingcopyfor a specific file extension, such as with--loader:.png=copy. -
Fix a regression in arrow function lowering (#2302)
This release fixes a regression with lowering arrow functions to function expressions in ES5. This feature was introduced in version 0.7.2 and regressed in version 0.14.30.
In JavaScript, regular
functionexpressions treatthisas an implicit argument that is determined by how the function is called, but arrow functions treatthisas a variable that is captured in the closure from the surrounding lexical scope. This is emulated in esbuild by storing the value ofthisin a variable before changing the arrow function into a function expression.However, the code that did this didn't treat
thisexpressions as a usage of that generated variable. Version 0.14.30 began omitting unused generated variables, which caused the transformation ofthisto break. This regression happened due to missing test coverage. With this release, the problem has been fixed:// Original code function foo() { return () => this } // Old output (with --target=es5) function foo() { return function() { return _this; }; } // New output (with --target=es5) function foo() { var _this = this; return function() { return _this; }; }This fix was contributed by @nkeynes.
-
Allow entity names as define values (#2292)
The "define" feature allows you to replace certain expressions with certain other expressions at compile time. For example, you might want to replace the global identifier
IS_PRODUCTIONwith the boolean valuetruewhen building for production. Previously the only expressions you could substitute in were either identifier expressions or anything that is valid JSON syntax. This limitation exists because supporting more complex expressions is more complex (for example, substituting in arequire()call could potentially pull in additional files, which would need to be handled). With this release, you can now also now define something as a member expression chain of the formfoo.abc.xyz. -
Implement package self-references (#2312)
This release implements a rarely-used feature in node where a package can import itself by name instead of using relative imports. You can read more about this feature here: https://nodejs.org/api/packages.html#self-referencing-a-package-using-its-name. For example, assuming the
package.jsonin a given package looks like this:// package.json { "name": "a-package", "exports": { ".": "./main.mjs", "./foo": "./foo.js" } }Then any module in that package can reference an export in the package itself:
// ./a-module.mjs import { something } from 'a-package'; // Imports "something" from ./main.mjs.Self-referencing is also available when using
require, both in an ES module, and in a CommonJS one. For example, this code will also work:// ./a-module.js const { something } = require('a-package/foo'); // Loads from ./foo.js. -
Add a warning for assigning to an import (#2319)
Import bindings are immutable in JavaScript, and assigning to them will throw an error. So instead of doing this:
import { foo } from 'foo' foo++You need to do something like this instead:
import { foo, setFoo } from 'foo' setFoo(foo + 1)This is already an error if you try to bundle this code with esbuild. However, this was previously allowed silently when bundling is disabled, which can lead to confusion for people who don't know about this aspect of how JavaScript works. So with this release, there is now a warning when you do this:
▲ [WARNING] This assignment will throw because "foo" is an import [assign-to-import] example.js:2:0: 2 │ foo++ ╵ ~~~ Imports are immutable in JavaScript. To modify the value of this import, you must export a setter function in the imported file (e.g. "setFoo") and then import and call that function here instead.This new warning can be turned off with
--log-override:assign-to-import=silentif you don't want to see it. -
Implement
alwaysStrictintsconfig.json(#2264)This release adds
alwaysStrictto the set of TypeScripttsconfig.jsonconfiguration values that esbuild supports. When this is enabled, esbuild will forbid syntax that isn't allowed in strict mode and will automatically insert"use strict";at the top of generated output files. This matches the behavior of the TypeScript compiler: https://www.typescriptlang.org/tsconfig#alwaysStrict.
-
Fix TypeScript parse error whe a generic function is the first type argument (#2306)
In TypeScript, the
<<token may need to be split apart into two<tokens if it's present in a type argument context. This was already correctly handled for all type expressions and for identifier expressions such as in the following code:// These cases already worked in the previous release let foo: Array<<T>() => T>; bar<<T>() => T>;However, normal expressions of the following form were previously incorrectly treated as syntax errors:
// These cases were broken but have now been fixed foo.bar<<T>() => T>; foo?.<<T>() => T>();With this release, these cases now parsed correctly.
-
Fix minification regression with pure IIFEs (#2279)
An Immediately Invoked Function Expression (IIFE) is a function call to an anonymous function, and is a way of introducing a new function-level scope in JavaScript since JavaScript lacks a way to do this otherwise. And a pure function call is a function call with the special
/* @__PURE__ */comment before it, which tells JavaScript build tools that the function call can be considered to have no side effects (and can be removed if it's unused).Version 0.14.9 of esbuild introduced a regression that changed esbuild's behavior when these two features were combined. If the IIFE body contains a single expression, the resulting output still contained that expression instead of being empty. This is a minor regression because you normally wouldn't write code like this, so this shouldn't come up in practice, and it doesn't cause any correctness issues (just larger-than-necessary output). It's unusual that you would tell esbuild "remove this if the result is unused" and then not store the result anywhere, since the result is unused by construction. But regardless, the issue has now been fixed.
For example, the following code is a pure IIFE, which means it should be completely removed when minification is enabled. Previously it was replaced by the contents of the IIFE but it's now completely removed:
// Original code /* @__PURE__ */ (() => console.log(1))() // Old output (with --minify) console.log(1); // New output (with --minify) -
Add log messages for indirect
requirereferences (#2231)A long time ago esbuild used to warn about indirect uses of
requirebecause they break esbuild's ability to analyze the dependencies of the code and cause dependencies to not be bundled, resulting in a potentially broken bundle. However, this warning was removed because many people wanted the warning to be removed. Some packages have code that usesrequirelike this but on a code path that isn't used at run-time, so their code still happens to work even though the bundle is incomplete. For example, the following code will not bundlebindings:// Prevent React Native packager from seeing modules required with this const nodeRequire = require; function getRealmConstructor(environment) { switch (environment) { case "node.js": case "electron": return nodeRequire("bindings")("realm.node").Realm; } }Version 0.11.11 of esbuild removed this warning, which means people no longer have a way to know at compile time whether their bundle is broken in this way. Now that esbuild has custom log message levels, this warning can be added back in a way that should make both people happy. With this release, there is now a log message for this that defaults to the
debuglog level, which normally isn't visible. You can either do--log-override:indirect-require=warningto make this log message a warning (and therefore visible) or use--log-level=debugto see this and all otherdebuglog messages.
-
Fix a parser hang on invalid CSS (#2276)
Previously invalid CSS with unbalanced parentheses could cause esbuild's CSS parser to hang. An example of such an input is the CSS file
:x(. This hang has been fixed. -
Add support for custom log message levels
This release allows you to override the default log level of esbuild's individual log messages. For example, CSS syntax errors are treated as warnings instead of errors by default because CSS grammar allows for rules containing syntax errors to be ignored. However, if you would like for esbuild to consider CSS syntax errors to be build errors, you can now configure that like this:
-
CLI
$ esbuild example.css --log-override:css-syntax-error=error -
JS API
let result = await esbuild.build({ entryPoints: ['example.css'], logOverride: { 'css-syntax-error': 'error', }, }) -
Go API
result := api.Build(api.BuildOptions{ EntryPoints: []string{"example.ts"}, LogOverride: map[string]api.LogLevel{ "css-syntax-error": api.LogLevelError, }, })
You can also now use this feature to silence warnings that you are not interested in. Log messages are referred to by their identifier. Each identifier is stable (i.e. shouldn't change over time) except there is no guarantee that the log message will continue to exist. A given log message may potentially be removed in the future, in which case esbuild will ignore log levels set for that identifier. The current list of supported log level identifiers for use with this feature can be found below:
JavaScript:
assign-to-constantcall-import-namespacecommonjs-variable-in-esmdelete-super-propertydirect-evalduplicate-caseduplicate-object-keyempty-import-metaequals-nanequals-negative-zeroequals-new-objecthtml-comment-in-jsimpossible-typeofprivate-name-will-throwsemicolon-after-returnsuspicious-boolean-notthis-is-undefined-in-esmunsupported-dynamic-importunsupported-jsx-commentunsupported-regexpunsupported-require-call
CSS:
css-syntax-errorinvalid-@charsetinvalid-@importinvalid-@nestinvalid-@layerinvalid-calcjs-comment-in-cssunsupported-@charsetunsupported-@namespaceunsupported-css-property
Bundler:
different-path-caseignored-bare-importignored-dynamic-importimport-is-undefinedpackage.jsonrequire-resolve-not-externaltsconfig.json
Source maps:
invalid-source-mappingssections-in-source-mapmissing-source-mapunsupported-source-map-comment
Documentation about which identifiers correspond to which log messages will be added in the future, but hasn't been written yet. Note that it's not possible to configure the log level for a build error. This is by design because changing that would cause esbuild to incorrectly proceed in the building process generate invalid build output. You can only configure the log level for non-error log messages (although you can turn non-errors into errors).
-
-
Fix a minification regression in 0.14.40 (#2270, #2271, #2273)
Version 0.14.40 substituted string property keys with numeric property keys if the number has the same string representation as the original string. This was done in three places: computed member expressions, object literal properties, and class fields. However, negative numbers are only valid in computed member expressions while esbuild incorrectly applied this substitution for negative numbers in all places. This release fixes the regression by only doing this substitution for negative numbers in computed member expressions.
This fix was contributed by @susiwen8.
-
Correct esbuild's implementation of
"preserveValueImports": true(#2268)TypeScript's
preserveValueImportssetting tells the compiler to preserve unused imports, which can sometimes be necessary because otherwise TypeScript will remove unused imports as it assumes they are type annotations. This setting is useful for programming environments that strip TypeScript types as part of a larger code transformation where additional code is appended later that will then make use of those unused imports, such as with Svelte or Vue.This release fixes an issue where esbuild's implementation of
preserveValueImportsdiverged from the official TypeScript compiler. If the import clause is present but empty of values (even if it contains types), then the import clause should be considered a type-only import clause. This was an oversight, and has now been fixed:// Original code import "keep" import { k1 } from "keep" import k2, { type t1 } from "keep" import {} from "remove" import { type t2 } from "remove" // Old output under "preserveValueImports": true import "keep"; import { k1 } from "keep"; import k2, {} from "keep"; import {} from "remove"; import {} from "remove"; // New output under "preserveValueImports": true (matches the TypeScript compiler) import "keep"; import { k1 } from "keep"; import k2 from "keep"; -
Avoid regular expression syntax errors in older browsers (#2215)
Previously esbuild always passed JavaScript regular expression literals through unmodified from the input to the output. This is undesirable when the regular expression uses newer features that the configured target environment doesn't support. For example, the
dflag (i.e. the match indices feature) is new in ES2022 and doesn't work in older browsers. If esbuild generated a regular expression literal containing thedflag, then older browsers would consider esbuild's output to be a syntax error and none of the code would run.With this release, esbuild now detects when an unsupported feature is being used and converts the regular expression literal into a
new RegExp()constructor instead. One consequence of this is that the syntax error is transformed into a run-time error, which allows the output code to run (and to potentially handle the run-time error). Another consequence of this is that it allows you to include a polyfill that overwrites theRegExpconstructor in older browsers with one that supports modern features. Note that esbuild does not handle polyfills for you, so you will need to include aRegExppolyfill yourself if you want one.// Original code console.log(/b/d.exec('abc').indices) // New output (with --target=chrome90) console.log(/b/d.exec("abc").indices); // New output (with --target=chrome89) console.log(new RegExp("b", "d").exec("abc").indices);This is currently done transparently without a warning. If you would like to debug this transformation to see where in your code esbuild is transforming regular expression literals and why, you can pass
--log-level=debugto esbuild and review the information present in esbuild's debug logs. -
Add Opera to more internal feature compatibility tables (#2247, #2252)
The internal compatibility tables that esbuild uses to determine which environments support which features are derived from multiple sources. Most of it is automatically derived from these ECMAScript compatibility tables, but missing information is manually copied from MDN, GitHub PR comments, and various other websites. Version 0.14.35 of esbuild introduced Opera as a possible target environment which was automatically picked up by the compatibility table script, but the manually-copied information wasn't updated to include Opera. This release fixes this omission so Opera feature compatibility should now be accurate.
This was contributed by @lbwa.
-
Ignore
EPERMerrors on directories (#2261)Previously bundling with esbuild when inside a sandbox environment which does not have permission to access the parent directory did not work because esbuild would try to read the directory to search for a
node_modulesfolder and would then fail the build when that failed. In practice this caused issues with running esbuild withsandbox-execon macOS. With this release, esbuild will treat directories with permission failures as empty to allow for thenode_modulessearch to continue past the denied directory and into its parent directory. This means it should now be possible to bundle with esbuild in these situations. This fix is similar to the fix in version 0.9.1 but is forEPERMwhile that fix was forEACCES. -
Remove an irrelevant extra
"use strict"directive (#2264)The presence of a
"use strict"directive in the output file is controlled by the presence of one in the entry point. However, there was a bug that would include one twice if the output format is ESM. This bug has been fixed. -
Minify strings into integers inside computed properties (#2214)
This release now minifies
a["0"]intoa[0]when the result is equivalent:// Original code console.log(x['0'], { '0': x }, class { '0' = x }) // Old output (with --minify) console.log(x["0"],{"0":x},class{"0"=x}); // New output (with --minify) console.log(x[0],{0:x},class{0=x});This transformation currently only happens when the numeric property represents an integer within the signed 32-bit integer range.
-
Fix code generation for
export defaultand/* @__PURE__ */call (#2203)The
/* @__PURE__ */comment annotation can be added to function calls to indicate that they are side-effect free. These annotations are passed through into the output by esbuild since many JavaScript tools understand them. However, there was an edge case where printing this comment before a function call caused esbuild to fail to parenthesize a function literal because it thought it was no longer at the start of the expression. This problem has been fixed:// Original code export default /* @__PURE__ */ (function() { })() // Old output export default /* @__PURE__ */ function() { }(); // New output export default /* @__PURE__ */ (function() { })(); -
Preserve
...before JSX child expressions (#2245)TypeScript 4.5 changed how JSX child expressions that start with
...are emitted. Previously the...was omitted but starting with TypeScript 4.5, the...is now preserved instead. This release updates esbuild to match TypeScript's new output in this case:// Original code console.log(<a>{...b}</a>) // Old output console.log(/* @__PURE__ */ React.createElement("a", null, b)); // New output console.log(/* @__PURE__ */ React.createElement("a", null, ...b));Note that this behavior is TypeScript-specific. Babel doesn't support the
...token at all (it gives the error "Spread children are not supported in React"). -
Slightly adjust esbuild's handling of the
browserfield inpackage.json(#2239)This release changes esbuild's interpretation of
browserpath remapping to fix a regression that was introduced in esbuild version 0.14.21. Browserify has a bug where it incorrectly matches package paths to relative paths in thebrowserfield, and esbuild replicates this bug for compatibility with Browserify. I have a set of tests that I use to verify that esbuild's replication of this Browserify is accurate here: https://github.com/evanw/package-json-browser-tests. However, I was missing a test case and esbuild's behavior diverges from Browserify in this case. This release now handles this edge case as well:-
entry.js:require('pkg/sub') -
node_modules/pkg/package.json:{ "browser": { "./sub": "./sub/foo.js", "./sub/sub.js": "./sub/foo.js" } } -
node_modules/pkg/sub/foo.js:require('sub') -
node_modules/sub/index.js:console.log('works')
The import path
subinrequire('sub')was previously matching the remapping"./sub/sub.js": "./sub/foo.js"but with this release it should now no longer match that remapping. Nowrequire('sub')will only match the remapping"./sub/sub": "./sub/foo.js"(without the trailing.js). Browserify apparently only matches without the.jssuffix here. -
-
Further fixes to TypeScript 4.7 instantiation expression parsing (#2201)
This release fixes some additional edge cases with parsing instantiation expressions from the upcoming version 4.7 of TypeScript. Previously it was allowed for an instantiation expression to precede a binary operator but with this release, that's no longer allowed. This was sometimes valid in the TypeScript 4.7 beta but is no longer allowed in the latest version of TypeScript 4.7. Fixing this also fixed a regression that was introduced by the previous release of esbuild:
Code TS 4.6.3 TS 4.7.0 beta TS 4.7.0 nightly esbuild 0.14.36 esbuild 0.14.37 esbuild 0.14.38 a<b> == c<d>Invalid a == cInvalid a == ca == cInvalid a<b> in c<d>Invalid Invalid Invalid Invalid a in cInvalid a<b>>=c<d>Invalid Invalid Invalid Invalid a >= cInvalid a<b>=c<d>Invalid a < b >= ca = ca < b >= ca = ca = ca<b>>c<d>a < b >> ca < b >> ca < b >> ca < b >> ca > ca < b >> cThis table illustrates some of the more significant changes between all of these parsers. The most important part is that esbuild 0.14.38 now matches the behavior of the latest TypeScript compiler for all of these cases.
-
Add support for TypeScript's
moduleSuffixesfield from TypeScript 4.7The upcoming version of TypeScript adds the
moduleSuffixesfield totsconfig.jsonthat introduces more rules to import path resolution. SettingmoduleSuffixesto[".ios", ".native", ""]will try to look at the the relative files./foo.ios.ts,./foo.native.ts, and finally./foo.tsfor an import path of./foo. Note that the empty string""inmoduleSuffixesis necessary for TypeScript to also look-up./foo.ts. This was announced in the TypeScript 4.7 beta blog post. -
Match the new ASI behavior from TypeScript nightly builds (#2188)
This release updates esbuild to match some very recent behavior changes in the TypeScript parser regarding automatic semicolon insertion. For more information, see TypeScript issues #48711 and #48654 (I'm not linking to them directly to avoid Dependabot linkback spam on these issues due to esbuild's popularity). The result is that the following TypeScript code is now considered valid TypeScript syntax:
class A<T> {} new A<number> /* ASI now happens here */ if (0) {} interface B { (a: number): typeof a /* ASI now happens here */ <T>(): void }This fix was contributed by @g-plane.
-
Revert path metadata validation for now (#2177)
This release reverts the path metadata validation that was introduced in the previous release. This validation has uncovered a potential issue with how esbuild handles
onResolvecallbacks in plugins that will need to be fixed before path metadata validation is re-enabled.
-
Add support for parsing
typeofon #private fields from TypeScript 4.7 (#2174)The upcoming version of TypeScript now lets you use
#privatefields intypeoftype expressions:https://devblogs.microsoft.com/typescript/announcing-typescript-4-7-beta/#typeof-on-private-fields
class Container { #data = "hello!"; get data(): typeof this.#data { return this.#data; } set data(value: typeof this.#data) { this.#data = value; } }With this release, esbuild can now parse these new type expressions as well. This feature was contributed by @magic-akari.
-
Add Opera and IE to internal CSS feature support matrix (#2170)
Version 0.14.18 of esbuild added Opera and IE as available target environments, and added them to the internal JS feature support matrix. CSS feature support was overlooked, however. This release adds knowledge of Opera and IE to esbuild's internal CSS feature support matrix:
/* Original input */ a { color: rgba(0, 0, 0, 0.5); } /* Old output (with --target=opera49 --minify) */ a{color:rgba(0,0,0,.5)} /* New output (with --target=opera49 --minify) */ a{color:#00000080}The fix for this issue was contributed by @sapphi-red.
-
Change TypeScript class field behavior when targeting ES2022
TypeScript 4.3 introduced a breaking change where class field behavior changes from assign semantics to define semantics when the
targetsetting intsconfig.jsonis set toESNext. Specifically, the default value for TypeScript'suseDefineForClassFieldssetting when unspecified istrueif and only iftargetisESNext. TypeScript 4.6 introduced another change where this behavior now happens for bothESNextandES2022. Presumably this will be the case forES2023and up as well. With this release, esbuild's behavior has also been changed to match. Now configuring esbuild with--target=es2022will also cause TypeScript files to use the new class field behavior. -
Validate that path metadata returned by plugins is consistent
The plugin API assumes that all metadata for the same path returned by a plugin's
onResolvecallback is consistent. Previously this assumption was just assumed without any enforcement. Starting with this release, esbuild will now enforce this by generating a build error if this assumption is violated. The lack of validation has not been an issue (I have never heard of this being a problem), but it still seems like a good idea to enforce it. Here's a simple example of a plugin that generates inconsistentsideEffectsmetadata:let buggyPlugin = { name: 'buggy', setup(build) { let count = 0 build.onResolve({ filter: /^react$/ }, args => { return { path: require.resolve(args.path), sideEffects: count++ > 0, } }) }, }Since esbuild processes everything in parallel, the set of metadata that ends up being used for a given path is essentially random since it's whatever the task scheduler decides to schedule first. Thus if a plugin does not consistently provide the same metadata for a given path, subsequent builds may return different results. This new validation check prevents this problem.
Here's the new error message that's shown when this happens:
✘ [ERROR] [plugin buggy] Detected inconsistent metadata for the path "node_modules/react/index.js" when it was imported here: button.tsx:1:30: 1 │ import { createElement } from 'react' ╵ ~~~~~~~ The original metadata for that path comes from when it was imported here: app.tsx:1:23: 1 │ import * as React from 'react' ╵ ~~~~~~~ The difference in metadata is displayed below: { - "sideEffects": true, + "sideEffects": false, } This is a bug in the "buggy" plugin. Plugins provide metadata for a given path in an "onResolve" callback. All metadata provided for the same path must be consistent to ensure deterministic builds. Due to parallelism, one set of provided metadata will be randomly chosen for a given path, so providing inconsistent metadata for the same path can cause non-determinism. -
Suggest enabling a missing condition when
exportsmap fails (#2163)This release adds another suggestion to the error message that happens when an
exportsmap lookup fails if the failure could potentially be fixed by adding a missing condition. Here's what the new error message looks like (which now suggests--conditions=moduleas a possible workaround):✘ [ERROR] Could not resolve "@sentry/electron/main" index.js:1:24: 1 │ import * as Sentry from '@sentry/electron/main' ╵ ~~~~~~~~~~~~~~~~~~~~~~~ The path "./main" is not currently exported by package "@sentry/electron": node_modules/@sentry/electron/package.json:8:13: 8 │ "exports": { ╵ ^ None of the conditions provided ("require", "module") match any of the currently active conditions ("browser", "default", "import"): node_modules/@sentry/electron/package.json:16:14: 16 │ "./main": { ╵ ^ Consider enabling the "module" condition if this package expects it to be enabled. You can use "--conditions=module" to do that: node_modules/@sentry/electron/package.json:18:6: 18 │ "module": "./esm/main/index.js" ╵ ~~~~~~~~ Consider using a "require()" call to import this file, which will work because the "require" condition is supported by this package: index.js:1:24: 1 │ import * as Sentry from '@sentry/electron/main' ╵ ~~~~~~~~~~~~~~~~~~~~~~~ You can mark the path "@sentry/electron/main" as external to exclude it from the bundle, which will remove this error.This particular package had an issue where it was using the Webpack-specific
modulecondition without providing adefaultcondition. It looks like the intent in this case was to use the standardimportcondition instead. This specific change wasn't suggested here because this error message is for package consumers, not package authors.
Something went wrong with the publishing script for the previous release. Publishing again.
-
Fix a regression regarding
super(#2158)This fixes a regression from the previous release regarding classes with a super class, a private member, and a static field in the scenario where the static field needs to be lowered but where private members are supported by the configured target environment. In this scenario, esbuild could incorrectly inject the instance field initializers that use
thisinto the constructor before the call tosuper(), which is invalid. This problem has now been fixed (notice thatthisis now used aftersuper()instead of before):// Original code class Foo extends Object { static FOO; constructor() { super(); } #foo; } // Old output (with --bundle) var _foo; var Foo = class extends Object { constructor() { __privateAdd(this, _foo, void 0); super(); } }; _foo = new WeakMap(); __publicField(Foo, "FOO"); // New output (with --bundle) var _foo; var Foo = class extends Object { constructor() { super(); __privateAdd(this, _foo, void 0); } }; _foo = new WeakMap(); __publicField(Foo, "FOO");During parsing, esbuild scans the class and makes certain decisions about the class such as whether to lower all static fields, whether to lower each private member, or whether calls to
super()need to be tracked and adjusted. Previously esbuild made two passes through the class members to compute this information. However, with the newsuper()call lowering logic added in the previous release, we now need three passes to capture the whole dependency chain for this case: 1) lowering static fields requires 2) lowering private members which requires 3) adjustingsuper()calls.The reason lowering static fields requires lowering private members is because lowering static fields moves their initializers outside of the class body, where they can't access private members anymore. Consider this code:
class Foo { get #foo() {} static bar = new Foo().#foo }We can't just lower static fields without also lowering private members, since that causes a syntax error:
class Foo { get #foo() {} } Foo.bar = new Foo().#foo;And the reason lowering private members requires adjusting
super()calls is because the injected private member initializers usethis, which is only accessible aftersuper()calls in the constructor. -
Fix an issue with
--keep-namesnot keeping some names (#2149)This release fixes a regression with
--keep-namesfrom version 0.14.26. PR #2062 attempted to remove superfluous calls to the__namehelper function by omitting calls of the form__name(foo, "foo")where the name of the symbol in the first argument is equal to the string in the second argument. This was assuming that the initializer for the symbol would automatically be assigned the expected.nameproperty by the JavaScript VM, which turned out to be an incorrect assumption. To fix the regression, this PR has been reverted.The assumption is true in many cases but isn't true when the initializer is moved into another automatically-generated variable, which can sometimes be necessary during the various syntax transformations that esbuild does. For example, consider the following code:
class Foo { static get #foo() { return Foo.name } static get foo() { return this.#foo } } let Bar = Foo Foo = { name: 'Bar' } console.log(Foo.name, Bar.name)This code should print
Bar Foo. With--keep-names --target=es6that code is lowered by esbuild into the following code (omitting the helper function definitions for brevity):var _foo, foo_get; const _Foo = class { static get foo() { return __privateGet(this, _foo, foo_get); } }; let Foo = _Foo; __name(Foo, "Foo"); _foo = new WeakSet(); foo_get = /* @__PURE__ */ __name(function() { return _Foo.name; }, "#foo"); __privateAdd(Foo, _foo); let Bar = Foo; Foo = { name: "Bar" }; console.log(Foo.name, Bar.name);The injection of the automatically-generated
_Foovariable is necessary to preserve the semantics of the capturedFoobinding for methods defined within the class body, even when the definition needs to be moved outside of the class body during code transformation. Due to a JavaScript quirk, this binding is immutable and does not change even ifFoois later reassigned. The PR that was reverted was incorrectly removing the call to__name(Foo, "Foo"), which turned out to be necessary after all in this case. -
Print some large integers using hexadecimal when minifying (#2162)
When
--minifyis active, esbuild will now use one fewer byte to represent certain large integers:// Original code x = 123456787654321; // Old output (with --minify) x=123456787654321; // New output (with --minify) x=0x704885f926b1;This works because a hexadecimal representation can be shorter than a decimal representation starting at around 1012 and above.
This optimization made me realize that there's probably an opportunity to optimize printed numbers for smaller gzipped size instead of or in addition to just optimizing for minimal uncompressed byte count. The gzip algorithm does better with repetitive sequences, so for example
0xFFFFFFFFis probably a better representation than4294967295even though the byte counts are the same. As far as I know, no JavaScript minifier does this optimization yet. I don't know enough about how gzip works to know if this is a good idea or what the right metric for this might be. -
Add Linux ARM64 support for Deno (#2156)
This release adds Linux ARM64 support to esbuild's Deno API implementation, which allows esbuild to be used with Deno on a Raspberry Pi.
-
Fix
superusage in lowered private methods (#2039)Previously esbuild failed to transform
superproperty accesses inside private methods in the case when private methods have to be lowered because the target environment doesn't support them. The generated code still contained thesuperkeyword even though the method was moved outside of the class body, which is a syntax error in JavaScript. This release fixes this transformation issue and now produces valid code:// Original code class Derived extends Base { #foo() { super.foo() } bar() { this.#foo() } } // Old output (with --target=es6) var _foo, foo_fn; class Derived extends Base { constructor() { super(...arguments); __privateAdd(this, _foo); } bar() { __privateMethod(this, _foo, foo_fn).call(this); } } _foo = new WeakSet(); foo_fn = function() { super.foo(); }; // New output (with --target=es6) var _foo, foo_fn; const _Derived = class extends Base { constructor() { super(...arguments); __privateAdd(this, _foo); } bar() { __privateMethod(this, _foo, foo_fn).call(this); } }; let Derived = _Derived; _foo = new WeakSet(); foo_fn = function() { __superGet(_Derived.prototype, this, "foo").call(this); };Because of this change, lowered
superproperty accesses on instances were rewritten so that they can exist outside of the class body. This rewrite affects code generation for allsuperproperty accesses on instances including those inside loweredasyncfunctions. The new approach is different but should be equivalent to the old approach:// Original code class Foo { foo = async () => super.foo() } // Old output (with --target=es6) class Foo { constructor() { __publicField(this, "foo", () => { var __superGet = (key) => super[key]; return __async(this, null, function* () { return __superGet("foo").call(this); }); }); } } // New output (with --target=es6) class Foo { constructor() { __publicField(this, "foo", () => __async(this, null, function* () { return __superGet(Foo.prototype, this, "foo").call(this); })); } } -
Fix some tree-shaking bugs regarding property side effects
This release fixes some cases where side effects in computed properties were not being handled correctly. Specifically primitives and private names as properties should not be considered to have side effects, and object literals as properties should be considered to have side effects:
// Original code let shouldRemove = { [1]: 2 } let shouldRemove2 = class { #foo } let shouldKeep = class { [{ toString() { sideEffect() } }] } // Old output (with --tree-shaking=true) let shouldRemove = { [1]: 2 }; let shouldRemove2 = class { #foo; }; // New output (with --tree-shaking=true) let shouldKeep = class { [{ toString() { sideEffect(); } }]; }; -
Add the
wasmModuleoption to theinitializeJS API (#1093)The
initializeJS API must be called when using esbuild in the browser to provide the WebAssembly module for esbuild to use. Previously the only way to do that was using thewasmURLAPI option like this:await esbuild.initialize({ wasmURL: '/node_modules/esbuild-wasm/esbuild.wasm', }) console.log(await esbuild.transform('1+2'))With this release, you can now also initialize esbuild using a
WebAssembly.Moduleinstance using thewasmModuleAPI option instead. The example above is equivalent to the following code:await esbuild.initialize({ wasmModule: await WebAssembly.compileStreaming(fetch('/node_modules/esbuild-wasm/esbuild.wasm')) }) console.log(await esbuild.transform('1+2'))This could be useful for environments where you want more control over how the WebAssembly download happens or where downloading the WebAssembly module is not possible.
-
Add support for parsing "optional variance annotations" from TypeScript 4.7 (#2102)
The upcoming version of TypeScript now lets you specify
inand/orouton certain type parameters (specifically only on a type alias, an interface declaration, or a class declaration). These modifiers control type parameter covariance and contravariance:type Provider<out T> = () => T; type Consumer<in T> = (x: T) => void; type Mapper<in T, out U> = (x: T) => U; type Processor<in out T> = (x: T) => T;With this release, esbuild can now parse these new type parameter modifiers. This feature was contributed by @magic-akari.
-
Improve support for
super()constructor calls in nested locations (#2134)In JavaScript, derived classes must call
super()somewhere in theconstructormethod before being able to accessthis. Class public instance fields, class private instance fields, and TypeScript constructor parameter properties can all potentially cause code which usesthisto be inserted into the constructor body, which must be inserted after thesuper()call. To make these insertions straightforward to implement, the TypeScript compiler doesn't allow callingsuper()somewhere other than in a root-level statement in the constructor body in these cases.Previously esbuild's class transformations only worked correctly when
super()was called in a root-level statement in the constructor body, just like the TypeScript compiler. But with this release, esbuild should now generate correct code as long as the call tosuper()appears anywhere in the constructor body:// Original code class Foo extends Bar { constructor(public skip = false) { if (skip) { super(null) return } super({ keys: [] }) } } // Old output (incorrect) class Foo extends Bar { constructor(skip = false) { if (skip) { super(null); return; } super({ keys: [] }); this.skip = skip; } } // New output (correct) class Foo extends Bar { constructor(skip = false) { var __super = (...args) => { super(...args); this.skip = skip; }; if (skip) { __super(null); return; } __super({ keys: [] }); } } -
Add support for the new
@containerCSS rule (#2127)This release adds support for
@containerin CSS files. This means esbuild will now pretty-print and minify these rules better since it now better understands the internal structure of these rules:/* Original code */ @container (width <= 150px) { #inner { color: yellow; } } /* Old output (with --minify) */ @container (width <= 150px){#inner {color: yellow;}} /* New output (with --minify) */ @container (width <= 150px){#inner{color:#ff0}}This was contributed by @yisibl.
-
Avoid CSS cascade-dependent keywords in the
font-familyproperty (#2135)In CSS,
initial,inherit, andunsetare CSS-wide keywords which means they have special behavior when they are specified as a property value. For example, whilefont-family: 'Arial'(as a string) andfont-family: Arial(as an identifier) are the same,font-family: 'inherit'(as a string) uses the font family namedinheritbutfont-family: inherit(as an identifier) inherits the font family from the parent element. This means esbuild must not unquote these CSS-wide keywords (anddefault, which is also reserved) during minification to avoid changing the meaning of the minified CSS.The current draft of the new CSS Cascading and Inheritance Level 5 specification adds another concept called cascade-dependent keywords of which there are two:
revertandrevert-layer. This release of esbuild guards against unquoting these additional keywords as well to avoid accidentally breaking pages that use a font with the same name:/* Original code */ a { font-family: 'revert'; } b { font-family: 'revert-layer', 'Segoe UI', serif; } /* Old output (with --minify) */ a{font-family:revert}b{font-family:revert-layer,Segoe UI,serif} /* New output (with --minify) */ a{font-family:"revert"}b{font-family:"revert-layer",Segoe UI,serif}This fix was contributed by @yisibl.
-
Change the context of TypeScript parameter decorators (#2147)
While TypeScript parameter decorators are expressions, they are not evaluated where they exist in the code. They are moved to after the class declaration and evaluated there instead. Specifically this TypeScript code:
class Class { method(@decorator() arg) {} }becomes this JavaScript code:
class Class { method(arg) {} } __decorate([ __param(0, decorator()) ], Class.prototype, "method", null);This has several consequences:
-
Whether
awaitis allowed inside a decorator expression or not depends on whether the class declaration itself is in anasynccontext or not. With this release, you can now useawaitinside a decorator expression when the class declaration is either inside anasyncfunction or is at the top-level of an ES module and top-level await is supported. Note that the TypeScript compiler currently has a bug regarding this edge case: https://github.com/microsoft/TypeScript/issues/48509.// Using "await" inside a decorator expression is now allowed async function fn(foo: Promise<any>) { class Class { method(@decorator(await foo) arg) {} } return Class }Also while TypeScript currently allows
awaitto be used like this inasyncfunctions, it doesn't currently allowyieldto be used like this in generator functions. It's not yet clear whether this behavior withyieldis a bug or by design, so I haven't made any changes to esbuild's handling ofyieldinside decorator expressions in this release. -
Since the scope of a decorator expression is the scope enclosing the class declaration, they cannot access private identifiers. Previously this was incorrectly allowed but with this release, esbuild no longer allows this. Note that the TypeScript compiler currently has a bug regarding this edge case: https://github.com/microsoft/TypeScript/issues/48515.
// Using private names inside a decorator expression is no longer allowed class Class { static #priv = 123 method(@decorator(Class.#priv) arg) {} } -
Since the scope of a decorator expression is the scope enclosing the class declaration, identifiers inside parameter decorator expressions should never be resolved to a parameter of the enclosing method. Previously this could happen, which was a bug with esbuild. This bug no longer happens in this release.
// Name collisions now resolve to the outer name instead of the inner name let arg = 1 class Class { method(@decorator(arg) arg = 2) {} }Specifically previous versions of esbuild generated the following incorrect JavaScript (notice the use of
arg2):let arg = 1; class Class { method(arg2 = 2) { } } __decorateClass([ __decorateParam(0, decorator(arg2)) ], Class.prototype, "method", 1);This release now generates the following correct JavaScript (notice the use of
arg):let arg = 1; class Class { method(arg2 = 2) { } } __decorateClass([ __decorateParam(0, decorator(arg)) ], Class.prototype, "method", 1);
-
-
Fix some obscure edge cases with
superproperty accessThis release fixes the following obscure problems with
superwhen targeting an older JavaScript environment such as--target=es6:-
The compiler could previously crash when a lowered
asyncarrow function contained a class with a field initializer that used asuperproperty access:let foo = async () => class extends Object { bar = super.toString } -
The compiler could previously generate incorrect code when a lowered
asyncmethod of a derived class contained a nested class with a computed class member involving asuperproperty access on the derived class:class Base { foo() { return 'bar' } } class Derived extends Base { async foo() { return new class { [super.foo()] = 'success' } } } new Derived().foo().then(obj => console.log(obj.bar)) -
The compiler could previously generate incorrect code when a default-exported class contained a
superproperty access inside a lowered static private class field:class Foo { static foo = 123 } export default class extends Foo { static #foo = super.foo static bar = this.#foo }
-
-
Fix a minification bug with a double-nested
ifinside a label followed byelse(#2139)This fixes a minification bug that affects the edge case where
ifis followed byelseand theifcontains a label that contains a nestedif. Normally esbuild's AST printer automatically wraps the body of a single-statementifin braces to avoid the "dangling else"if/elseambiguity common to C-like languages (where theelseaccidentally becomes associated with the innerifinstead of the outerif). However, I was missing automatic wrapping of label statements, which did not have test coverage because they are a rarely-used feature. This release fixes the bug:// Original code if (a) b: { if (c) break b } else if (d) e() // Old output (with --minify) if(a)e:if(c)break e;else d&&e(); // New output (with --minify) if(a){e:if(c)break e}else d&&e(); -
Fix edge case regarding
baseUrlandpathsintsconfig.json(#2119)In
tsconfig.json, TypeScript forbids non-relative values insidepathsifbaseUrlis not present, and esbuild does too. However, TypeScript checked this after the entiretsconfig.jsonhierarchy was parsed while esbuild incorrectly checked this immediately when parsing the file containing thepathsmap. This caused incorrect warnings to be generated fortsconfig.jsonfiles that specify abaseUrlvalue and that inherit apathsvalue from anextendsclause. Now esbuild will only check for non-relativepathsvalues after the entire hierarchy has been parsed to avoid generating incorrect warnings. -
Better handle errors where the esbuild binary executable is corrupted or missing (#2129)
If the esbuild binary executable is corrupted or missing, previously there was one situation where esbuild's JavaScript API could hang instead of generating an error. This release changes esbuild's library code to generate an error instead in this case.
-
Add support for some new CSS rules (#2115, #2116, #2117)
This release adds support for
@font-palette-values,@counter-style, and@font-feature-values. This means esbuild will now pretty-print and minify these rules better since it now better understands the internal structure of these rules:/* Original code */ @font-palette-values --Foo { base-palette: 1; } @counter-style bar { symbols: b a r; } @font-feature-values Bop { @styleset { test: 1; } } /* Old output (with --minify) */ @font-palette-values --Foo{base-palette: 1;}@counter-style bar{symbols: b a r;}@font-feature-values Bop{@styleset {test: 1;}} /* New output (with --minify) */ @font-palette-values --Foo{base-palette:1}@counter-style bar{symbols:b a r}@font-feature-values Bop{@styleset{test:1}} -
Upgrade to Go 1.18.0 (#2105)
Binary executables for this version are now published with Go version 1.18.0. The Go release notes say that the linker generates smaller binaries and that on 64-bit ARM chips, compiled binaries run around 10% faster. On an M1 MacBook Pro, esbuild's benchmark runs approximately 8% faster than before and the binary executable is approximately 4% smaller than before.
This also fixes a regression from version 0.14.26 of esbuild where the browser builds of the
esbuild-wasmpackage could fail to be bundled due to the use of built-in node libraries. The primary WebAssembly shim for Go 1.18.0 no longer uses built-in node libraries.
-
Avoid generating an enumerable
defaultimport for CommonJS files in Babel mode (#2097)Importing a CommonJS module into an ES module can be done in two different ways. In node mode the
defaultimport is always set tomodule.exports, while in Babel mode thedefaultimport passes through tomodule.exports.defaultinstead. Node mode is triggered when the importing file ends in.mjs, hastype: "module"in itspackage.jsonfile, or the imported module does not have a__esModulemarker.Previously esbuild always created the forwarding
defaultimport in Babel mode, even ifmodule.exportshad no property calleddefault. This was problematic because the getter nameddefaultstill showed up as a property on the imported namespace object, and could potentially interfere with code that iterated over the properties of the imported namespace object. With this release the getter nameddefaultwill now only be added in Babel mode if thedefaultproperty exists at the time of the import. -
Fix a circular import edge case regarding ESM-to-CommonJS conversion (#1894, #2059)
This fixes a regression that was introduced in version 0.14.5 of esbuild. Ever since that version, esbuild now creates two separate export objects when you convert an ES module file into a CommonJS module: one for ES modules and one for CommonJS modules. The one for CommonJS modules is written to
module.exportsand exported from the file, and the one for ES modules is internal and can be accessed by bundling code that imports the entry point (for example, the entry point might import itself to be able to inspect its own exports).The reason for these two separate export objects is that CommonJS modules are supposed to see a special export called
__esModulewhich indicates that the module used to be an ES module, while ES modules are not supposed to see any automatically-added export named__esModule. This matters for real-world code both because people sometimes iterate over the properties of ES module export namespace objects and because some people write ES module code containing their own exports named__esModulethat they expect other ES module code to be able to read.However, this change to split exports into two separate objects broke ES module re-exports in the edge case where the imported module is involved in an import cycle. This happened because the CommonJS
module.exportsobject was no longer mutated as exports were added. Instead it was being initialized at the end of the generated file after the import statements to other modules (which are converted intorequire()calls). This release changesmodule.exportsinitialization to happen earlier in the file and then double-writes further exports to both the ES module and CommonJS module export objects.This fix was contributed by @indutny.
-
Fix a tree shaking regression regarding
vardeclarations (#2080, #2085, #2098, #2099)Version 0.14.8 of esbuild enabled removal of duplicate function declarations when minification is enabled (see #610):
// Original code function x() { return 1 } console.log(x()) function x() { return 2 } // Output (with --minify-syntax) console.log(x()); function x() { return 2; }This transformation is safe because function declarations are "hoisted" in JavaScript, which means they are all done first before any other code is evaluted. This means the last function declaration will overwrite all previous function declarations with the same name.
However, this introduced an unintentional regression for
vardeclarations in which all but the last declaration was dropped if tree-shaking was enabled. This only happens for top-levelvardeclarations that re-declare the same variable multiple times. This regression has now been fixed:// Original code var x = 1 console.log(x) var x = 2 // Old output (with --tree-shaking=true) console.log(x); var x = 2; // New output (with --tree-shaking=true) var x = 1; console.log(x); var x = 2;This case now has test coverage.
-
Add support for parsing "instantiation expressions" from TypeScript 4.7 (#2038)
The upcoming version of TypeScript now lets you specify
<...>type parameters on a JavaScript identifier without using a call expression:const ErrorMap = Map<string, Error>; // new () => Map<string, Error> const errorMap = new ErrorMap(); // Map<string, Error>With this release, esbuild can now parse these new type annotations. This feature was contributed by @g-plane.
-
Avoid
new Functionin esbuild's library code (#2081)Some JavaScript environments such as Cloudflare Workers or Deno Deploy don't allow
new Functionbecause they disallow dynamic JavaScript evaluation. Previously esbuild's WebAssembly-based library used this to construct the WebAssembly worker function. With this release, the code is now inlined without usingnew Functionso it will be able to run even when this restriction is in place. -
Drop superfluous
__name()calls (#2062)When the
--keep-namesoption is specified, esbuild inserts calls to a__namehelper function to ensure that the.nameproperty on function and class objects remains consistent even if the function or class name is renamed to avoid a name collision or because name minification is enabled. With this release, esbuild will now try to omit these calls to the__namehelper function when the name of the function or class object was not renamed during the linking process after all:// Original code import { foo as foo1 } from 'data:text/javascript,export function foo() { return "foo1" }' import { foo as foo2 } from 'data:text/javascript,export function foo() { return "foo2" }' console.log(foo1.name, foo2.name) // Old output (with --bundle --keep-names) (() => { var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); function foo() { return "foo1"; } __name(foo, "foo"); function foo2() { return "foo2"; } __name(foo2, "foo"); console.log(foo.name, foo2.name); })(); // New output (with --bundle --keep-names) (() => { var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); function foo() { return "foo1"; } function foo2() { return "foo2"; } __name(foo2, "foo"); console.log(foo.name, foo2.name); })();Notice how one of the calls to
__nameis now no longer printed. This change was contributed by @indutny.
-
Reduce minification of CSS transforms to avoid Safari bugs (#2057)
In Safari, applying a 3D CSS transform to an element can cause it to render in a different order than applying a 2D CSS transform even if the transformation matrix is identical. I believe this is a bug in Safari because the CSS
transformspecification doesn't seem to distinguish between 2D and 3D transforms as far as rendering order:For elements whose layout is governed by the CSS box model, any value other than
nonefor thetransformproperty results in the creation of a stacking context.This bug means that minifying a 3D transform into a 2D transform must be avoided even though it's a valid transformation because it can cause rendering differences in Safari. Previously esbuild sometimes minified 3D CSS transforms into 2D CSS transforms but with this release, esbuild will no longer do that:
/* Original code */ div { transform: matrix3d(2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1) } /* Old output (with --minify) */ div{transform:scale(2)} /* New output (with --minify) */ div{transform:scale3d(2,2,1)} -
Minification now takes advantage of the
?.operatorThis adds new code minification rules that shorten code with the
?.optional chaining operator when the result is equivalent:// Original code let foo = (x) => { if (x !== null && x !== undefined) x.y() return x === null || x === undefined ? undefined : x.z } // Old output (with --minify) let foo=n=>(n!=null&&n.y(),n==null?void 0:n.z); // New output (with --minify) let foo=n=>(n?.y(),n?.z);This only takes effect when minification is enabled and when the configured target environment is known to support the optional chaining operator. As always, make sure to set
--target=to the appropriate language target if you are running the minified code in an environment that doesn't support the latest JavaScript features. -
Add source mapping information for some non-executable tokens (#1448)
Code coverage tools can generate reports that tell you if any code exists that has not been run (or "covered") during your tests. You can use this information to add additional tests for code that isn't currently covered.
Some popular JavaScript code coverage tools have bugs where they incorrectly consider lines without any executable code as uncovered, even though there's no test you could possibly write that would cause those lines to be executed. For example, they apparently complain about the lines that only contain the trailing
}token of an object literal.With this release, esbuild now generates source mappings for some of these trailing non-executable tokens. This may not successfully work around bugs in code coverage tools because there are many non-executable tokens in JavaScript and esbuild doesn't map them all (the drawback of mapping these extra tokens is that esbuild will use more memory, build more slowly, and output a bigger source map). The true solution is to fix the bugs in the code coverage tools in the first place.
-
Fall back to WebAssembly on Android x64 (#2068)
Go's compiler supports trivial cross-compiling to almost all platforms without installing any additional software other than the Go compiler itself. This has made it very easy for esbuild to publish native binary executables for many platforms. However, it strangely doesn't support cross-compiling to Android x64 without installing the Android build tools. So instead of publishing a native esbuild binary executable to npm, this release publishes a WebAssembly fallback build. This is essentially the same as the
esbuild-wasmpackage but it's installed automatically when you install theesbuildpackage on Android x64. So packages that depend on theesbuildpackage should now work on Android x64. If you want to use a native binary executable of esbuild on Android x64, you may be able to build it yourself from source after installing the Android build tools. -
Update to Go 1.17.8
The version of the Go compiler used to compile esbuild has been upgraded from Go 1.17.7 to Go 1.17.8, which fixes the RISC-V 64-bit build. Compiler optimizations for the RISC-V 64-bit build have now been re-enabled.
-
Allow
es2022as a target environment (#2012)TypeScript recently added support for
es2022as a compilation target so esbuild now supports this too. Support for this is preliminary as there is no published ES2022 specification yet (i.e. https://tc39.es/ecma262/2021/ exists but https://tc39.es/ecma262/2022/ is a 404 error). The meaning of esbuild'ses2022target may change in the future when the specification is finalized. Right now I have made thees2022target enable support for the syntax-related finished proposals that are marked as2022:- Class fields
- Class private members
- Class static blocks
- Ergonomic class private member checks
- Top-level await
I have also included the "arbitrary module namespace names" feature since I'm guessing it will end up in the ES2022 specification (this syntax feature was added to the specification without a proposal). TypeScript has not added support for this yet.
-
Match
defineto strings in index expressions (#2050)With this release, configuring
--define:foo.bar=baznow matches and replaces bothfoo.barandfoo['bar']expressions in the original source code. This is necessary for people who have enabled TypeScript'snoPropertyAccessFromIndexSignaturefeature, which prevents you from using normal property access syntax on a type with an index signature such as in the following code:declare let foo: { [key: string]: any } foo.bar // This is a type error if noPropertyAccessFromIndexSignature is enabled foo['bar']Previously esbuild would generate the following output with
--define:foo.bar=baz:baz; foo["bar"];Now esbuild will generate the following output instead:
baz; baz; -
Add
--mangle-quotedto mangle quoted properties (#218)The
--mangle-props=flag tells esbuild to automatically rename all properties matching the provided regular expression to shorter names to save space. Previously esbuild never modified the contents of string literals. In particular,--mangle-props=_would manglefoo._barbut notfoo['_bar']. There are some coding patterns where renaming quoted property names is desirable, such as when using TypeScript'snoPropertyAccessFromIndexSignaturefeature or when using TypeScript's discriminated union narrowing behavior:interface Foo { _foo: string } interface Bar { _bar: number } declare const value: Foo | Bar console.log('_foo' in value ? value._foo : value._bar)The
'_foo' in valuecheck tells TypeScript to narrow the type ofvaluetoFooin the true branch and toBarin the false branch. Previously esbuild didn't mangle the property name'_foo'because it was inside a string literal. With this release, you can now use--mangle-quotedto also rename property names inside string literals:// Old output (with --mangle-props=_) console.log("_foo" in value ? value.a : value.b); // New output (with --mangle-props=_ --mangle-quoted) console.log("a" in value ? value.a : value.b); -
Parse and discard TypeScript
export as namespacestatements (#2070)TypeScript
.d.tstype declaration files can sometimes contain statements of the formexport as namespace foo;. I believe these serve to declare that the module adds a property of that name to the global object. You aren't supposed to feed.d.tsfiles to esbuild so this normally doesn't matter, but sometimes esbuild can end up having to parse them. One such case is if you import a type-only package who'smainfield inpackage.jsonis a.d.tsfile.Previously esbuild only allowed
export as namespacestatements inside adeclarecontext:declare module Foo { export as namespace foo; }Now esbuild will also allow these statements outside of a
declarecontext:export as namespace foo;These statements are still just ignored and discarded.
-
Strip import assertions from unrecognized
import()expressions (#2036)The new "import assertions" JavaScript language feature adds an optional second argument to dynamic
import()expressions, which esbuild does support. However, this optional argument must be stripped when targeting older JavaScript environments for which this second argument would be a syntax error. Previously esbuild failed to strip this second argument in cases when the first argument toimport()wasn't a string literal. This problem is now fixed:// Original code console.log(import(foo, { assert: { type: 'json' } })) // Old output (with --target=es6) console.log(import(foo, { assert: { type: "json" } })); // New output (with --target=es6) console.log(import(foo)); -
Remove simplified statement-level literal expressions (#2063)
With this release, esbuild now removes simplified statement-level expressions if the simplified result is a literal expression even when minification is disabled. Previously this was only done when minification is enabled. This change was only made because some people are bothered by seeing top-level literal expressions. This change has no effect on code behavior.
-
Ignore
.d.tsrules inpathsintsconfig.jsonfiles (#2074, #2075)TypeScript's
tsconfig.jsonconfiguration file has apathsfield that lets you remap import paths to alternative files on the file system. This field is interpreted by esbuild during bundling so that esbuild's behavior matches that of the TypeScript type checker. However, people sometimes override import paths to JavaScript files to instead point to a.d.tsTypeScript type declaration file for that JavaScript file. The intent of this is to just use the remapping for type information and not to actually import the.d.tsfile during the build.With this release, esbuild will now ignore rules in
pathsthat result in a.d.tsfile during path resolution. This means code that does this should now be able to be bundled without modifying itstsconfig.jsonfile to remove the.d.tsrule. This change was contributed by @magic-akari. -
Disable Go compiler optimizations for the Linux RISC-V 64bit build (#2035)
Go's RISC-V 64bit compiler target has a fatal compiler optimization bug that causes esbuild to crash when it's run: https://github.com/golang/go/issues/51101. As a temporary workaround until a version of the Go compiler with the fix is published, Go compiler optimizations have been disabled for RISC-V. The 7.7mb esbuild binary executable for RISC-V is now 8.7mb instead. This workaround was contributed by @piggynl.
-
Update feature database to indicate that node 16.14+ supports import assertions (#2030)
Node versions 16.14 and above now support import assertions according to these release notes. This release updates esbuild's internal feature compatibility database with this information, so esbuild no longer strips import assertions with
--target=node16.14:// Original code import data from './package.json' assert { type: 'json' } console.log(data) // Old output (with --target=node16.14) import data from "./package.json"; console.log(data); // New output (with --target=node16.14) import data from "./package.json" assert { type: "json" }; console.log(data); -
Basic support for CSS
@layerrules (#2027)This adds basic parsing support for a new CSS feature called
@layerthat changes how the CSS cascade works. Adding parsing support for this rule to esbuild means esbuild can now minify the contents of@layerrules:/* Original code */ @layer a { @layer b { div { color: yellow; margin: 0.0px; } } } /* Old output (with --minify) */ @layer a{@layer b {div {color: yellow; margin: 0px;}}} /* New output (with --minify) */ @layer a.b{div{color:#ff0;margin:0}}You can read more about
@layerhere:- Documentation: https://developer.mozilla.org/en-US/docs/Web/CSS/@layer
- Motivation: https://developer.chrome.com/blog/cascade-layers/
Note that the support added in this release is only for parsing and printing
@layerrules. The bundler does not yet know about these rules and bundling with@layermay result in behavior changes since these new rules have unusual ordering constraints that behave differently than all other CSS rules. Specifically the order is derived from the first instance while with every other CSS rule, the order is derived from the last instance.
-
Preserve whitespace for token lists that look like CSS variable declarations (#2020)
Previously esbuild removed the whitespace after the CSS variable declaration in the following CSS:
/* Original input */ @supports (--foo: ){html{background:green}} /* Previous output */ @supports (--foo:){html{background:green}}However, that broke rendering in Chrome as it caused Chrome to ignore the entire rule. This did not break rendering in Firefox and Safari, so there's a browser bug either with Chrome or with both Firefox and Safari. In any case, esbuild now preserves whitespace after the CSS variable declaration in this case.
-
Ignore legal comments when merging adjacent duplicate CSS rules (#2016)
This release now generates more compact minified CSS when there are legal comments in between two adjacent rules with identical content:
/* Original code */ a { color: red } /* @preserve */ b { color: red } /* Old output (with --minify) */ a{color:red}/* @preserve */b{color:red} /* New output (with --minify) */ a,b{color:red}/* @preserve */ -
Block
onResolveandonLoaduntilonStartends (#1967)This release changes the semantics of the
onStartcallback. AllonStartcallbacks from all plugins are run concurrently so that a slow plugin doesn't hold up the entire build. That's still the case. However, previously the only thing waiting for theonStartcallbacks to finish was the end of the build. This meant thatonResolveand/oronLoadcallbacks could sometimes run beforeonStarthad finished. This was by design but violated user expectations. With this release, allonStartcallbacks must finish before anyonResolveand/oronLoadcallbacks are run. -
Add a self-referential
defaultexport to the JS API (#1897)Some people try to use esbuild's API using
import esbuild from 'esbuild'instead ofimport * as esbuild from 'esbuild'(i.e. using a default import instead of a namespace import). There is nodefaultexport so that wasn't ever intended to work. But it would work sometimes depending on which tools you used and how they were configured so some people still wrote code this way. This release tries to make that work by adding a self-referentialdefaultexport that is equal to esbuild's module namespace object.More detail: The published package for esbuild's JS API is in CommonJS format, although the source code for esbuild's JS API is in ESM format. The original ESM code for esbuild's JS API has no export named
defaultso using a default import like this doesn't work with Babel-compatible toolchains (since they respect the semantics of the original ESM code). However, it happens to work with node-compatible toolchains because node's implementation of importing CommonJS from ESM broke compatibility with existing conventions and automatically creates adefaultexport which is set tomodule.exports. This is an unfortunate compatibility headache because it means thedefaultimport only works sometimes. This release tries to fix this by explicitly creating a self-referentialdefaultexport. It now doesn't matter if you doesbuild.build(),esbuild.default.build(), oresbuild.default.default.build()because they should all do the same thing. Hopefully this means people don't have to deal with this problem anymore. -
Handle
writeerrors when esbuild's child process is killed (#2007)If you type Ctrl+C in a terminal when a script that uses esbuild's JS library is running, esbuild's child process may be killed before the parent process. In that case calls to the
write()syscall may fail with anEPIPEerror. Previously this resulted in an uncaught exception because esbuild didn't handle this case. Starting with this release, esbuild should now catch these errors and redirect them into a generalThe service was stoppederror which should be returned from whatever top-level API calls were in progress. -
Better error message when browser WASM bugs are present (#1863)
Safari's WebAssembly implementation appears to be broken somehow, at least when running esbuild. Sometimes this manifests as a stack overflow and sometimes as a Go panic. Previously a Go panic resulted in the error message
Can't find variable: fsbut this should now result in the Go panic being printed to the console. Using esbuild's WebAssembly library in Safari is still broken but now there's a more helpful error message.More detail: When Go panics, it prints a stack trace to stderr (i.e. file descriptor 2). Go's WebAssembly shim calls out to node's
fs.writeSync()function to do this, and it converts calls tofs.writeSync()into calls toconsole.log()in the browser by providing a shim forfs. However, Go's shim code stores the shim onwindow.fsin the browser. This is undesirable because it pollutes the global scope and leads to brittle code that can break if other code also useswindow.fs. To avoid this, esbuild shadows the global object by wrapping Go's shim. But that broke bare references tofssince the shim is no longer stored onwindow.fs. This release now stores the shim in a local variable namedfsso that bare references tofswork correctly. -
Undo incorrect dead-code elimination with destructuring (#1183)
Previously esbuild eliminated these statements as dead code if tree-shaking was enabled:
let [a] = {} let { b } = nullThis is incorrect because both of these lines will throw an error when evaluated. With this release, esbuild now preserves these statements even when tree shaking is enabled.
-
Update to Go 1.17.7
The version of the Go compiler used to compile esbuild has been upgraded from Go 1.17.6 to Go 1.17.7, which contains a few compiler and security bug fixes.
-
Handle an additional
browsermap edge case (#2001, #2002)There is a community convention around the
browserfield inpackage.jsonthat allows remapping import paths within a package when the package is bundled for use within a browser. There isn't a rigorous definition of how it's supposed to work and every bundler implements it differently. The approach esbuild uses is to try to be "maximally compatible" in that if at least one bundler exhibits a particular behavior regarding thebrowsermap that allows a mapping to work, then esbuild also attempts to make that work.I have a collection of test cases for this going here: https://github.com/evanw/package-json-browser-tests. However, I was missing test coverage for the edge case where a package path import in a subdirectory of the package could potentially match a remapping. The "maximally compatible" approach means replicating bugs in Browserify's implementation of the feature where package paths are mistaken for relative paths and are still remapped. Here's a specific example of an edge case that's now handled:
-
entry.js:require('pkg/sub') -
node_modules/pkg/package.json:{ "browser": { "./sub": "./sub/foo.js", "./sub/sub": "./sub/bar.js" } } -
node_modules/pkg/sub/foo.js:require('sub') -
node_modules/pkg/sub/bar.js:console.log('works')
The import path
subinrequire('sub')is mistaken for a relative path by Browserify due to a bug in Browserify, so Browserify treats it as if it were./subinstead. This is a Browserify-specific behavior and currently doesn't happen in any other bundler (except for esbuild, which attempts to replicate Browserify's bug).Previously esbuild was incorrectly resolving
./subrelative to the top-level package directory instead of to the subdirectory in this case, which meant./subwas incorrectly matching"./sub": "./sub/foo.js"instead of"./sub/sub": "./sub/bar.js". This has been fixed so esbuild can now emulate Browserify's bug correctly in this edge case. -
-
Support for esbuild with Linux on RISC-V 64bit (#2000)
With this release, esbuild now has a published binary executable for the RISC-V 64bit architecture in the
esbuild-linux-riscv64npm package. This change was contributed by @piggynl.
-
Fix property mangling and keyword properties (#1998)
Previously enabling property mangling with
--mangle-props=failed to add a space before property names after a keyword. This bug has been fixed:// Original code class Foo { static foo = { get bar() {} } } // Old output (with --minify --mangle-props=.) class Foo{statics={gett(){}}} // New output (with --minify --mangle-props=.) class Foo{static s={get t(){}}}
-
Special-case
constinlining at the top of a scope (#1317, #1981)The minifier now inlines
constvariables (even across modules during bundling) if a certain set of specific requirements are met:- All
constvariables to be inlined are at the top of their scope - That scope doesn't contain any
importorexportstatements with paths - All constants to be inlined are
null,undefined,true,false, an integer, or a short real number - Any expression outside of a small list of allowed ones stops constant identification
Practically speaking this basically means that you can trigger this optimization by just putting the constants you want inlined into a separate file (e.g.
constants.js) and bundling everything together.These specific conditions are present to avoid esbuild unintentionally causing any behavior changes by inlining constants when the variable reference could potentially be evaluated before being declared. It's possible to identify more cases where constants can be inlined but doing so may require complex call graph analysis so it has not been implemented. Although these specific heuristics may change over time, this general approach to constant inlining should continue to work going forward.
Here's an example:
// Original code const bold = 1 << 0; const italic = 1 << 1; const underline = 1 << 2; const font = bold | italic | underline; console.log(font); // Old output (with --minify --bundle) (()=>{var o=1<<0,n=1<<1,c=1<<2,t=o|n|c;console.log(t);})(); // New output (with --minify --bundle) (()=>{console.log(7);})(); - All
-
Add the
--mangle-cache=feature (#1977)This release adds a cache API for the newly-released
--mangle-props=feature. When enabled, all mangled property renamings are recorded in the cache during the initial build. Subsequent builds reuse the renamings stored in the cache and add additional renamings for any newly-added properties. This has a few consequences:-
You can customize what mangled properties are renamed to by editing the cache before passing it to esbuild (the cache is a map of the original name to the mangled name).
-
The cache serves as a list of all properties that were mangled. You can easily scan it to see if there are any unexpected property renamings.
-
You can disable mangling for individual properties by setting the renamed value to
falseinstead of to a string. This is similar to the--reserve-props=setting but on a per-property basis. -
You can ensure consistent renaming between builds (e.g. a main-thread file and a web worker, or a library and a plugin). Without this feature, each build would do an independent renaming operation and the mangled property names likely wouldn't be consistent.
Here's how to use it:
-
CLI
$ esbuild example.ts --mangle-props=_$ --mangle-cache=cache.json -
JS API
let result = await esbuild.build({ entryPoints: ['example.ts'], mangleProps: /_$/, mangleCache: { customRenaming_: '__c', disabledRenaming_: false, }, }) let updatedMangleCache = result.mangleCache -
Go API
result := api.Build(api.BuildOptions{ EntryPoints: []string{"example.ts"}, MangleProps: "_$", MangleCache: map[string]interface{}{ "customRenaming_": "__c", "disabledRenaming_": false, }, }) updatedMangleCache := result.MangleCache
The above code would do something like the following:
// Original code x = { customRenaming_: 1, disabledRenaming_: 2, otherProp_: 3, } // Generated code x = { __c: 1, disabledRenaming_: 2, a: 3 }; // Updated mangle cache { "customRenaming_": "__c", "disabledRenaming_": false, "otherProp_": "a" } -
-
Add
operaandieas possible target environmentsYou can now target Opera and/or Internet Explorer using the
--target=setting. For example,--target=opera45,ie9targets Opera 45 and Internet Explorer 9. This change does not add any additional features to esbuild's code transformation pipeline to transform newer syntax so that it works in Internet Explorer. It just adds information about what features are supported in these browsers to esbuild's internal feature compatibility table. -
Minify
typeof x !== 'undefined'totypeof x < 'u'This release introduces a small improvement for code that does a lot of
typeofchecks againstundefined:// Original code y = typeof x !== 'undefined'; // Old output (with --minify) y=typeof x!="undefined"; // New output (with --minify) y=typeof x<"u";This transformation is only active when minification is enabled, and is disabled if the language target is set lower than ES2020 or if Internet Explorer is set as a target environment. Before ES2020, implementations were allowed to return non-standard values from the
typeofoperator for a few objects. Internet Explorer took advantage of this to sometimes return the string'unknown'instead of'undefined'. But this has been removed from the specification and Internet Explorer was the only engine to do this, so this minification is valid for code that does not need to target Internet Explorer.
-
Attempt to fix an install script issue on Ubuntu Linux (#1711)
There have been some reports of esbuild failing to install on Ubuntu Linux for a while now. I haven't been able to reproduce this myself due to lack of reproduction instructions until today, when I learned that the issue only happens when you install node from the Snap Store instead of downloading the official version of node.
The problem appears to be that when node is installed from the Snap Store, install scripts are run with stderr not being writable? This then appears to cause a problem for esbuild's install script when it uses
execFileSyncto validate that the esbuild binary is working correctly. This throws the errorEACCES: permission denied, writeeven though this particular command never writes to stderr.Node's documentation says that stderr for
execFileSyncdefaults to that of the parent process. Forcing it to'pipe'instead appears to fix the issue, although I still don't fully understand what's happening or why. I'm publishing this small change regardless to see if it fixes this install script edge case. -
Avoid a syntax error due to
--mangle-props=.andsuper()(#1976)This release fixes an issue where passing
--mangle-props=.(i.e. telling esbuild to mangle every single property) caused a syntax error with code like this:class Foo {} class Bar extends Foo { constructor() { super(); } }The problem was that
constructorwas being renamed to another method, which then made it no longer a constructor, which meant thatsuper()was now a syntax error. I have added a workaround that avoids renaming any property namedconstructorso that esbuild doesn't generate a syntax error here.Despite this fix, I highly recommend not using
--mangle-props=.because your code will almost certainly be broken. You will have to manually add every single property that you don't want mangled to--reserve-props=which is an excessive maintenance burden (e.g. reserveparseto useJSON.parse). Instead I recommend using a common pattern for all properties you intend to be mangled that is unlikely to appear in the APIs you use such as "ends in an underscore." This is an opt-in approach instead of an opt-out approach. It also makes it obvious when reading the code which properties will be mangled and which ones won't be.
-
Support property name mangling with some TypeScript syntax features
The newly-released
--mangle-props=feature previously only affected JavaScript syntax features. This release adds support for using mangle props with certain TypeScript syntax features:-
TypeScript parameter properties
Parameter properties are a TypeScript-only shorthand way of initializing a class field directly from the constructor argument list. Previously parameter properties were not treated as properties to be mangled. They should now be handled correctly:
// Original code class Foo { constructor(public foo_) {} } new Foo().foo_; // Old output (with --minify --mangle-props=_) class Foo{constructor(c){this.foo_=c}}new Foo().o; // New output (with --minify --mangle-props=_) class Foo{constructor(o){this.c=o}}new Foo().c; -
TypeScript namespaces
Namespaces are a TypeScript-only way to add properties to an object. Previously exported namespace members were not treated as properties to be mangled. They should now be handled correctly:
// Original code namespace ns { export let foo_ = 1; export function bar_(x) {} } ns.bar_(ns.foo_); // Old output (with --minify --mangle-props=_) var ns;(e=>{e.foo_=1;function t(a){}e.bar_=t})(ns||={}),ns.e(ns.o); // New output (with --minify --mangle-props=_) var ns;(e=>{e.e=1;function o(p){}e.t=o})(ns||={}),ns.t(ns.e);
-
-
Fix property name mangling for lowered class fields
This release fixes a compiler crash with
--mangle-props=and class fields that need to be transformed to older versions of JavaScript. The problem was that doing this is an unusual case where the mangled property name must be represented as a string instead of as a property name, which previously wasn't implemented. This case should now work correctly:// Original code class Foo { static foo_; } Foo.foo_ = 0; // New output (with --mangle-props=_ --target=es6) class Foo { } __publicField(Foo, "a"); Foo.a = 0;
-
Add property name mangling with
--mangle-props=(#218)⚠️ Using this feature can break your code in subtle ways. Do not use this feature unless you know what you are doing, and you know exactly how it will affect both your code and all of your dependencies. ⚠️
This release introduces property name mangling, which is similar to an existing feature from the popular UglifyJS and Terser JavaScript minifiers. This setting lets you pass a regular expression to esbuild to tell esbuild to automatically rename all properties that match this regular expression. It's useful when you want to minify certain property names in your code either to make the generated code smaller or to somewhat obfuscate your code's intent.
Here's an example that uses the regular expression
_$to mangle all properties ending in an underscore, such asfoo_:$ echo 'console.log({ foo_: 0 }.foo_)' | esbuild --mangle-props=_$ console.log({ a: 0 }.a);Only mangling properties that end in an underscore is a reasonable heuristic because normal JS code doesn't typically contain identifiers like that. Browser APIs also don't use this naming convention so this also avoids conflicts with browser APIs. If you want to avoid mangling names such as
__defineGetter__you could consider using a more complex regular expression such as[^_]_$(i.e. must end in a non-underscore followed by an underscore).This is a separate setting instead of being part of the minify setting because it's an unsafe transformation that does not work on arbitrary JavaScript code. It only works if the provided regular expression matches all of the properties that you want mangled and does not match any of the properties that you don't want mangled. It also only works if you do not under any circumstances reference a property name to be mangled as a string. For example, it means you can't use
Object.defineProperty(obj, 'prop', ...)orobj['prop']with a mangled property. Specifically the following syntax constructs are the only ones eligible for property mangling:Syntax Example Dot property access x.foo_Dot optional chain x?.foo_Object properties x = { foo_: y }Object methods x = { foo_() {} }Class fields class x { foo_ = y }Class methods class x { foo_() {} }Object destructuring binding let { foo_: x } = yObject destructuring assignment ({ foo_: x } = y)JSX element names <X.foo_></X.foo_>JSX attribute names <X foo_={y} />You can avoid property mangling for an individual property by quoting it as a string. However, you must consistently use quotes or no quotes for a given property everywhere for this to work. For example,
print({ foo_: 0 }.foo_)will be mangled intoprint({ a: 0 }.a)whileprint({ 'foo_': 0 }['foo_'])will not be mangled.When using this feature, keep in mind that property names are only consistently mangled within a single esbuild API call but not across esbuild API calls. Each esbuild API call does an independent property mangling operation so output files generated by two different API calls may mangle the same property to two different names, which could cause the resulting code to behave incorrectly.
If you would like to exclude certain properties from mangling, you can reserve them with the
--reserve-props=setting. For example, this uses the regular expression^__.*__$to reserve all properties that start and end with two underscores, such as__foo__:$ echo 'console.log({ __foo__: 0 }.__foo__)' | esbuild --mangle-props=_$ console.log({ a: 0 }.a); $ echo 'console.log({ __foo__: 0 }.__foo__)' | esbuild --mangle-props=_$ "--reserve-props=^__.*__$" console.log({ __foo__: 0 }.__foo__); -
Mark esbuild as supporting node v12+ (#1970)
Someone requested that esbuild populate the
engines.nodefield inpackage.json. This release adds the following to eachpackage.jsonfile that esbuild publishes:"engines": { "node": ">=12" },This was chosen because it's the oldest version of node that's currently still receiving support from the node team, and so is the oldest version of node that esbuild supports: https://nodejs.org/en/about/releases/.
-
Remove error recovery for invalid
//comments in CSS (#1965)Previously esbuild treated
//as a comment in CSS and generated a warning, even though comments in CSS use/* ... */instead. This allowed you to run esbuild on CSS intended for certain CSS preprocessors that support single-line comments.However, some people are changing from another build tool to esbuild and have a code base that relies on
//being preserved even though it's nonsense CSS and causes the entire surrounding rule to be discarded by the browser. Presumably this nonsense CSS ended up there at some point due to an incorrectly-configured build pipeline and the site now relies on that entire rule being discarded. If esbuild interprets//as a comment, it could cause the rule to no longer be discarded or even cause something else to happen.With this release, esbuild no longer treats
//as a comment in CSS. It still warns about it but now passes it through unmodified. This means it's no longer possible to run esbuild on CSS code containing single-line comments but it means that esbuild's behavior regarding these nonsensical CSS rules more accurately represents what happens in a browser.
-
Fix bug with filename hashes and the
fileloader (#1957)This release fixes a bug where if a file name template has the
[hash]placeholder (either--entry-names=or--chunk-names=), the hash that esbuild generates didn't include the content of the string generated by thefileloader. Importing a file with thefileloader causes the imported file to be copied to the output directory and causes the imported value to be the relative path from the output JS file to that copied file. This bug meant that if the--asset-names=setting also contained[hash]and the file loaded with thefileloader was changed, the hash in the copied file name would change but the hash of the JS file would not change, which could potentially result in a stale JS file being loaded. Now the hash of the JS file will be changed too which fixes the reload issue. -
Prefer the
importcondition for entry points (#1956)The
exportsfield inpackage.jsonmaps package subpaths to file paths. The mapping can be conditional, which lets it vary in different situations. For example, you can have animportcondition that applies when the subpath originated from a JS import statement, and arequirecondition that applies when the subpath originated from a JS require call. These are supposed to be mutually exclusive according to the specification: https://nodejs.org/api/packages.html#conditional-exports.However, there's a situation with esbuild where it's not immediately obvious which one should be applied: when a package name is specified as an entry point. For example, this can happen if you do
esbuild --bundle some-pkgon the command line. In this situationsome-pkgdoes not originate from either a JS import statement or a JS require call. Previously esbuild just didn't apply theimportorrequireconditions. But that could result in path resolution failure if the package doesn't provide a back-updefaultcondition, as is the case with theis-plain-objectpackage.Starting with this release, esbuild will now use the
importcondition in this case. This appears to be how Webpack and Rollup handle this situation so this change makes esbuild consistent with other tools in the ecosystem. Parcel (the other major bundler) just doesn't handle this case at all so esbuild's behavior is not at odds with Parcel's behavior here. -
Make parsing of invalid
@keyframesrules more robust (#1959)This improves esbuild's parsing of certain malformed
@keyframesrules to avoid them affecting the following rule. This fix only affects invalid CSS files, and does not change any behavior for files containing valid CSS. Here's an example of the fix:/* Original code */ @keyframes x { . } @keyframes y { 1% { a: b; } } /* Old output (with --minify) */ @keyframes x{y{1% {a: b;}}} /* New output (with --minify) */ @keyframes x{.}@keyframes y{1%{a:b}}
-
Be more consistent about external paths (#619)
The rules for marking paths as external using
--external:grew over time as more special-cases were added. This release reworks the internal representation to be more straightforward and robust. A side effect is that wildcard patterns can now match post-resolve paths in addition to pre-resolve paths. Specifically you can now do--external:./node_modules/*to mark all files in the./node_modules/directory as external.This is the updated logic:
-
Before path resolution begins, import paths are checked against everything passed via an
--external:flag. In addition, if something looks like a package path (i.e. doesn't start with/or./or../), import paths are checked to see if they have that package path as a path prefix (so--external:@foo/barmatches the import path@foo/bar/baz). -
After path resolution ends, the absolute paths are checked against everything passed via
--external:that doesn't look like a package path (i.e. that starts with/or./or../). But before checking, the pattern is transformed to be relative to the current working directory.
-
-
Attempt to explain why esbuild can't run (#1819)
People sometimes try to install esbuild on one OS and then copy the
node_modulesdirectory over to another OS without reinstalling. This works with JavaScript code but doesn't work with esbuild because esbuild is a native binary executable. This release attempts to offer a helpful error message when this happens. It looks like this:$ ./node_modules/.bin/esbuild ./node_modules/esbuild/bin/esbuild:106 throw new Error(` ^ Error: You installed esbuild on another platform than the one you're currently using. This won't work because esbuild is written with native code and needs to install a platform-specific binary executable. Specifically the "esbuild-linux-arm64" package is present but this platform needs the "esbuild-darwin-arm64" package instead. People often get into this situation by installing esbuild on Windows or macOS and copying "node_modules" into a Docker image that runs Linux, or by copying "node_modules" between Windows and WSL environments. If you are installing with npm, you can try not copying the "node_modules" directory when you copy the files over, and running "npm ci" or "npm install" on the destination platform after the copy. Or you could consider using yarn instead which has built-in support for installing a package on multiple platforms simultaneously. If you are installing with yarn, you can try listing both this platform and the other platform in your ".yarnrc.yml" file using the "supportedArchitectures" feature: https://yarnpkg.com/configuration/yarnrc/#supportedArchitectures Keep in mind that this means multiple copies of esbuild will be present. Another alternative is to use the "esbuild-wasm" package instead, which works the same way on all platforms. But it comes with a heavy performance cost and can sometimes be 10x slower than the "esbuild" package, so you may also not want to do that. at generateBinPath (./node_modules/esbuild/bin/esbuild:106:17) at Object.<anonymous> (./node_modules/esbuild/bin/esbuild:161:39) at Module._compile (node:internal/modules/cjs/loader:1101:14) at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10) at Module.load (node:internal/modules/cjs/loader:981:32) at Function.Module._load (node:internal/modules/cjs/loader:822:12) at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12) at node:internal/main/run_main_module:17:47
-
Ignore invalid
@importrules in CSS (#1946)In CSS,
@importrules must come first before any other kind of rule (except for@charsetrules). Previously esbuild would warn about incorrectly ordered@importrules and then hoist them to the top of the file. This broke people who wrote invalid@importrules in the middle of their files and then relied on them being ignored. With this release, esbuild will now ignore invalid@importrules and pass them through unmodified. This more accurately follows the CSS specification. Note that this behavior differs from other tools like Parcel, which does hoist CSS@importrules. -
Print invalid CSS differently (#1947)
This changes how esbuild prints nested
@importstatements that are missing a trailing;, which is invalid CSS. The result is still partially invalid CSS, but now printed in a better-looking way:/* Original code */ .bad { @import url("other") } .red { background: red; } /* Old output (with --minify) */ .bad{@import url(other) } .red{background: red;}} /* New output (with --minify) */ .bad{@import url(other);}.red{background:red} -
Warn about CSS nesting syntax (#1945)
There's a proposed CSS syntax for nesting rules using the
&selector, but it's not currently implemented in any browser. Previously esbuild silently passed the syntax through untransformed. With this release, esbuild will now warn when you use nesting syntax with a--target=setting that includes a browser. -
Warn about
}and>inside JSX elementsThe
}and>characters are invalid inside JSX elements according to the JSX specification because they commonly result from typos like these that are hard to catch in code reviews:function F() { return <div>></div>; } function G() { return <div>{1}}</div>; }The TypeScript compiler already treats this as an error, so esbuild now treats this as an error in TypeScript files too. That looks like this:
✘ [ERROR] The character ">" is not valid inside a JSX element example.tsx:2:14: 2 │ return <div>></div>; │ ^ ╵ {'>'} Did you mean to escape it as "{'>'}" instead? ✘ [ERROR] The character "}" is not valid inside a JSX element example.tsx:5:17: 5 │ return <div>{1}}</div>; │ ^ ╵ {'}'} Did you mean to escape it as "{'}'}" instead?Babel doesn't yet treat this as an error, so esbuild only warns about these characters in JavaScript files for now. Babel 8 treats this as an error but Babel 8 hasn't been released yet. If you see this warning, I recommend fixing the invalid JSX syntax because it will become an error in the future.
-
Warn about basic CSS property typos
This release now generates a warning if you use a CSS property that is one character off from a known CSS property:
▲ [WARNING] "marign-left" is not a known CSS property example.css:2:2: 2 │ marign-left: 12px; │ ~~~~~~~~~~~ ╵ margin-left Did you mean "margin-left" instead?
-
Fix a bug with enum inlining (#1903)
The new TypeScript enum inlining behavior had a bug where it worked correctly if you used
export enum Foobut not if you usedenum Fooand then laterexport { Foo }. This release fixes the bug so enum inlining now works correctly in this case. -
Warn about
module.exports.foo = ...in ESM (#1907)The
modulevariable is treated as a global variable reference instead of as a CommonJS module reference in ESM code, which can cause problems for people that try to use both CommonJS and ESM exports in the same file. There has been a warning about this since version 0.14.9. However, the warning only covered cases likeexports.foo = barandmodule.exports = barbut notmodule.exports.foo = bar. This last case is now handled;▲ [WARNING] The CommonJS "module" variable is treated as a global variable in an ECMAScript module and may not work as expected example.ts:2:0: 2 │ module.exports.b = 1 ╵ ~~~~~~ This file is considered to be an ECMAScript module because of the "export" keyword here: example.ts:1:0: 1 │ export let a = 1 ╵ ~~~~~~ -
Enable esbuild's CLI with Deno (#1913)
This release allows you to use Deno as an esbuild installer, without also needing to use esbuild's JavaScript API. You can now use esbuild's CLI with Deno:
deno run --allow-all "https://deno.land/x/esbuild@v0.14.11/mod.js" --version
-
Enable tree shaking of classes with lowered static fields (#175)
If the configured target environment doesn't support static class fields, they are converted into a call to esbuild's
__publicFieldfunction instead. However, esbuild's tree-shaking pass treated this call as a side effect, which meant that all classes with static fields were ineligible for tree shaking. This release fixes the problem by explicitly ignoring calls to the__publicFieldfunction during tree shaking side-effect determination. Tree shaking is now enabled for these classes:// Original code class Foo { static foo = 'foo' } class Bar { static bar = 'bar' } new Bar() // Old output (with --tree-shaking=true --target=es6) class Foo { } __publicField(Foo, "foo", "foo"); class Bar { } __publicField(Bar, "bar", "bar"); new Bar(); // New output (with --tree-shaking=true --target=es6) class Bar { } __publicField(Bar, "bar", "bar"); new Bar(); -
Treat
--define:foo=undefinedas an undefined literal instead of an identifier (#1407)References to the global variable
undefinedare automatically replaced with the literal value for undefined, which appears asvoid 0when printed. This allows for additional optimizations such as collapsingundefined ?? barinto justbar. However, this substitution was not done for values specified via--define:. As a result, esbuild could potentially miss out on certain optimizations in these cases. With this release, it's now possible to use--define:to substitute something with an undefined literal:// Original code let win = typeof window !== 'undefined' ? window : {} // Old output (with --define:window=undefined --minify) let win=typeof undefined!="undefined"?undefined:{}; // New output (with --define:window=undefined --minify) let win={}; -
Add the
--drop:debuggerflag (#1809)Passing this flag causes all
debugger;statements to be removed from the output. This is similar to thedrop_debugger: trueflag available in the popular UglifyJS and Terser JavaScript minifiers. -
Add the
--drop:consoleflag (#28)Passing this flag causes all
console.xyz()API calls to be removed from the output. This is similar to thedrop_console: trueflag available in the popular UglifyJS and Terser JavaScript minifiers.WARNING: Using this flag can introduce bugs into your code! This flag removes the entire call expression including all call arguments. If any of those arguments had important side effects, using this flag will change the behavior of your code. Be very careful when using this flag. If you want to remove console API calls without removing arguments with side effects (which does not introduce bugs), you should mark the relevant API calls as pure instead like this:
--pure:console.log --minify. -
Inline calls to certain no-op functions when minifying (#290, #907)
This release makes esbuild inline two types of no-op functions: empty functions and identity functions. These most commonly arise when most of the function body is eliminated as dead code. In the examples below, this happens because we use
--define:window.DEBUG=falseto cause dead code elimination inside the function body of the resultingif (false)statement. This inlining is a small code size and performance win but, more importantly, it allows for people to use these features to add useful abstractions that improve the development experience without needing to worry about the run-time performance impact.An identity function is a function that just returns its argument. Here's an example of inlining an identity function:
// Original code function logCalls(fn) { if (window.DEBUG) return function(...args) { console.log('calling', fn.name, 'with', args) return fn.apply(this, args) } return fn } export const foo = logCalls(function foo() {}) // Old output (with --minify --define:window.DEBUG=false --tree-shaking=true) function o(n){return n}export const foo=o(function(){}); // New output (with --minify --define:window.DEBUG=false --tree-shaking=true) export const foo=function(){};An empty function is a function with an empty body. Here's an example of inlining an empty function:
// Original code function assertNotNull(val: Object | null): asserts val is Object { if (window.DEBUG && val === null) throw new Error('null assertion failed'); } export const val = getFoo(); assertNotNull(val); console.log(val.bar); // Old output (with --minify --define:window.DEBUG=false --tree-shaking=true) function l(o){}export const val=getFoo();l(val);console.log(val.bar); // New output (with --minify --define:window.DEBUG=false --tree-shaking=true) export const val=getFoo();console.log(val.bar);To get this behavior you'll need to use the
functionkeyword to define your function since that causes the definition to be hoisted, which eliminates concerns around initialization order. These features also work across modules, so functions are still inlined even if the definition of the function is in a separate module from the call to the function. To get cross-module function inlining to work, you'll need to have bundling enabled and use theimportandexportkeywords to access the function so that esbuild can see which functions are called. And all of this has been added without an observable impact to compile times.I previously wasn't able to add this to esbuild easily because of esbuild's low-pass compilation approach. The compiler only does three full passes over the data for speed. The passes are roughly for parsing, binding, and printing. It's only possible to inline something after binding but it needs to be inlined before printing. Also the way module linking was done made it difficult to roll back uses of symbols that were inlined, so the symbol definitions were not tree shaken even when they became unused due to inlining.
The linking issue was somewhat resolved when I fixed #128 in the previous release. To implement cross-module inlining of TypeScript enums, I came up with a hack to defer certain symbol uses until the linking phase, which happens after binding but before printing. Another hack is that inlining of TypeScript enums is done directly in the printer to avoid needing another pass.
The possibility of these two hacks has unblocked these simple function inlining use cases that are now handled. This isn't a fully general approach because optimal inlining is recursive. Inlining something may open up further inlining opportunities, which either requires multiple iterations or a worklist algorithm, both of which don't work when doing late-stage inlining in the printer. But the function inlining that esbuild now implements is still useful even though it's one level deep, and so I believe it's still worth adding.
-
Implement cross-module tree shaking of TypeScript enum values (#128)
If your bundle uses TypeScript enums across multiple files, esbuild is able to inline the enum values as long as you export and import the enum using the ES module
exportandimportkeywords. However, this previously still left the definition of the enum in the bundle even when it wasn't used anymore. This was because esbuild's tree shaking (i.e. dead code elimination) is based on information recorded during parsing, and at that point we don't know which imported symbols are inlined enum values and which aren't.With this release, esbuild will now remove enum definitions that become unused due to cross-module enum value inlining. Property accesses off of imported symbols are now tracked separately during parsing and then resolved during linking once all inlined enum values are known. This behavior change means esbuild's support for cross-module inlining of TypeScript enums is now finally complete. Here's an example:
// entry.ts import { Foo } from './enum' console.log(Foo.Bar) // enum.ts export enum Foo { Bar }Bundling the example code above now results in the enum definition being completely removed from the bundle:
// Old output (with --bundle --minify --format=esm) var r=(o=>(o[o.Bar=0]="Bar",o))(r||{});console.log(0); // New output (with --bundle --minify --format=esm) console.log(0); -
Fix a regression with
export {} fromand CommonJS (#1890)This release fixes a regression that was introduced by the change in 0.14.7 that avoids calling the
__toESMwrapper for import statements that are converted torequirecalls and that don't use thedefaultor__esModuleexport names. The previous change was correct for theimport {} fromsyntax but not for theexport {} fromsyntax, which meant that in certain cases with re-exported values, the value of thedefaultimport could be different than expected. This release fixes the regression. -
Warn about using
moduleorexportsin ESM code (#1887)CommonJS export variables cannot be referenced in ESM code. If you do this, they are treated as global variables instead. This release includes a warning for people that try to use both CommonJS and ES module export styles in the same file. Here's an example:
export enum Something { a, b, } module.exports = { a: 1, b: 2 }Running esbuild on that code now generates a warning that looks like this:
▲ [WARNING] The CommonJS "module" variable is treated as a global variable in an ECMAScript module and may not work as expected example.ts:5:0: 5 │ module.exports = { a: 1, b: 2 } ╵ ~~~~~~ This file is considered to be an ECMAScript module because of the "export" keyword here: example.ts:1:0: 1 │ export enum Something { ╵ ~~~~~~
-
Add a
resolveAPI for plugins (#641, #1652)Plugins now have access to a new API called
resolvethat runs esbuild's path resolution logic and returns the result to the caller. This lets you write plugins that can reuse esbuild's complex built-in path resolution logic to change the inputs and/or adjust the outputs. Here's an example:let examplePlugin = { name: 'example', setup(build) { build.onResolve({ filter: /^example$/ }, async () => { const result = await build.resolve('./foo', { resolveDir: '/bar' }) if (result.errors.length > 0) return result return { ...result, external: true } }) }, }This plugin intercepts imports to the path
example, tells esbuild to resolve the import./fooin the directory/bar, and then forces whatever path esbuild returns to be considered external. Here are some additional details:-
If you don't pass the optional
resolveDirparameter, esbuild will still runonResolveplugin callbacks but will not attempt any path resolution itself. All of esbuild's path resolution logic depends on theresolveDirparameter including looking for packages innode_modulesdirectories (since it needs to know where thosenode_modulesdirectories might be). -
If you want to resolve a file name in a specific directory, make sure the input path starts with
./. Otherwise the input path will be treated as a package path instead of a relative path. This behavior is identical to esbuild's normal path resolution logic. -
If path resolution fails, the
errorsproperty on the returned object will be a non-empty array containing the error information. This function does not always throw an error when it fails. You need to check for errors after calling it. -
The behavior of this function depends on the build configuration. That's why it's a property of the
buildobject instead of being a top-level API call. This also means you can't call it until all pluginsetupfunctions have finished since these give plugins the opportunity to adjust the build configuration before it's frozen at the start of the build. So the newresolvefunction is going to be most useful inside youronResolveand/oronLoadcallbacks. -
There is currently no attempt made to detect infinite path resolution loops. Calling
resolvefrom withinonResolvewith the same parameters is almost certainly a bad idea.
-
-
Avoid the CJS-to-ESM wrapper in some cases (#1831)
Import statements are converted into
require()calls when the output format is set to CommonJS. To convert from CommonJS semantics to ES module semantics, esbuild wraps the return value in a call to esbuild's__toESM()helper function. However, the conversion is only needed if it's possible that the exports nameddefaultor__esModulecould be accessed.This release avoids calling this helper function in cases where esbuild knows it's impossible for the
defaultor__esModuleexports to be accessed, which results in smaller and faster code. To get this behavior, you have to use theimport {} fromimport syntax:// Original code import { readFile } from "fs"; readFile(); // Old output (with --format=cjs) var __toESM = (module, isNodeMode) => { ... }; var import_fs = __toESM(require("fs")); (0, import_fs.readFile)(); // New output (with --format=cjs) var import_fs = require("fs"); (0, import_fs.readFile)(); -
Strip overwritten function declarations when minifying (#610)
JavaScript allows functions to be re-declared, with each declaration overwriting the previous declaration. This type of code can sometimes be emitted by automatic code generators. With this release, esbuild now takes this behavior into account when minifying to drop all but the last declaration for a given function:
// Original code function foo() { console.log(1) } function foo() { console.log(2) } // Old output (with --minify) function foo(){console.log(1)}function foo(){console.log(2)} // New output (with --minify) function foo(){console.log(2)} -
Add support for the Linux IBM Z 64-bit Big Endian platform (#1864)
With this release, the esbuild package now includes a Linux binary executable for the IBM System/390 64-bit architecture. This new platform was contributed by @shahidhs-ibm.
-
Allow whitespace around
:in JSX elements (#1877)This release allows you to write the JSX
<rdf:Description rdf:ID="foo" />as<rdf : Description rdf : ID="foo" />instead. Doing this is not forbidden by the JSX specification. While this doesn't work in TypeScript, it does work with other JSX parsers in the ecosystem, so support for this has been added to esbuild.
-
Cross-module inlining of TypeScript
enumconstants (#128)This release adds inlining of TypeScript
enumconstants across separate modules. It activates when bundling is enabled and when the enum is exported via theexportkeyword and imported via theimportkeyword:// foo.ts export enum Foo { Bar } // bar.ts import { Foo } from './foo.ts' console.log(Foo.Bar)The access to
Foo.Barwill now be compiled into0 /* Bar */even though the enum is defined in a separate file. This inlining was added without adding another pass (which would have introduced a speed penalty) by splitting the code for the inlining between the existing parsing and printing passes. Enum inlining is active whether or not you useenumorconst enumbecause it improves performance.To demonstrate the performance improvement, I compared the performance of the TypeScript compiler built by bundling the TypeScript compiler source code with esbuild before and after this change. The speed of the compiler was measured by using it to type check a small TypeScript code base. Here are the results:
tscwith esbuild 0.14.6 with esbuild 0.14.7 Time 2.96s 3.45s 2.95s As you can see, enum inlining gives around a 15% speedup, which puts the esbuild-bundled version at the same speed as the offical TypeScript compiler build (the
tsccolumn)!The specifics of the benchmark aren't important here since it's just a demonstration of how enum inlining can affect performance. But if you're wondering, I type checked the Rollup code base using a work-in-progress branch of the TypeScript compiler that's part of the ongoing effort to convert their use of namespaces into ES modules.
-
Mark node built-in modules as having no side effects (#705)
This release marks node built-in modules such as
fsas being side-effect free. That means unused imports to these modules are now removed when bundling, which sometimes results in slightly smaller code. For example:// Original code import fs from 'fs'; import path from 'path'; console.log(path.delimiter); // Old output (with --bundle --minify --platform=node --format=esm) import"fs";import o from"path";console.log(o.delimiter); // New output (with --bundle --minify --platform=node --format=esm) import o from"path";console.log(o.delimiter);Note that these modules are only automatically considered side-effect when bundling for node, since they are only known to be side-effect free imports in that environment. However, you can customize this behavior with a plugin by returning
external: trueandsideEffects: falsein anonResolvecallback for whatever paths you want to be treated this way. -
Recover from a stray top-level
}in CSS (#1876)This release fixes a bug where a stray
}at the top-level of a CSS file would incorrectly truncate the remainder of the file in the output (although not without a warning). With this release, the remainder of the file is now still parsed and printed:/* Original code */ .red { color: red; } } .blue { color: blue; } .green { color: green; } /* Old output (with --minify) */ .red{color:red} /* New output (with --minify) */ .red{color:red}} .blue{color:#00f}.green{color:green}This fix was contributed by @sbfaulkner.
-
Fix a minifier bug with BigInt literals
Previously expression simplification optimizations in the minifier incorrectly assumed that numeric operators always return numbers. This used to be true but has no longer been true since the introduction of BigInt literals in ES2020. Now numeric operators can return either a number or a BigInt depending on the arguments. This oversight could potentially have resulted in behavior changes. For example, this code printed
falsebefore being minified andtrueafter being minified because esbuild shortened===to==under the false assumption that both operands were numbers:var x = 0; console.log((x ? 2 : -1n) === -1);The type checking logic has been rewritten to take into account BigInt literals in this release, so this incorrect simplification is no longer applied.
-
Enable removal of certain unused template literals (#1853)
This release contains improvements to the minification of unused template literals containing primitive values:
// Original code `${1}${2}${3}`; `${x ? 1 : 2}${y}`; // Old output (with --minify) ""+1+2+3,""+(x?1:2)+y; // New output (with --minify) x,`${y}`;This can arise when the template literals are nested inside of another function call that was determined to be unnecessary such as an unused call to a function marked with the
/* @__PURE__ */pragma.This release also fixes a bug with this transformation where minifying the unused expression
`foo ${bar}`into"" + barchanged the meaning of the expression. Template string interpolation always callstoStringwhile string addition may callvalueOfinstead. This unused expression is now minified to`${bar}`, which is slightly longer but which avoids the behavior change. -
Allow
keyof/readonly/inferin TypeScript index signatures (#1859)This release fixes a bug that prevented these keywords from being used as names in index signatures. The following TypeScript code was previously rejected, but is now accepted:
interface Foo { [keyof: string]: number }This fix was contributed by @magic-akari.
-
Avoid warning about
import.metaif it's replaced (#1868)It's possible to replace the
import.metaexpression using the--define:feature. Previously doing that still warned that theimport.metasyntax was not supported when targeting ES5. With this release, there will no longer be a warning in this case.
-
Fix an issue with the publishing script
This release fixes a missing dependency issue in the publishing script where it was previously possible for the published binary executable to have an incorrect version number.
-
Adjust esbuild's handling of
defaultexports and the__esModulemarker (#532, #1591, #1719)This change requires some background for context. Here's the history to the best of my understanding:
When the ECMAScript module
import/exportsyntax was being developed, the CommonJS module format (used in Node.js) was already widely in use. Because of this the export name calleddefaultwas given special a syntax. Instead of writingimport { default as foo } from 'bar'you can just writeimport foo from 'bar'. The idea was that when ECMAScript modules (a.k.a. ES modules) were introduced, you could import existing CommonJS modules using the new import syntax for compatibility. Since CommonJS module exports are dynamic while ES module exports are static, it's not generally possible to determine a CommonJS module's export names at module instantiation time since the code hasn't been evaluated yet. So the value ofmodule.exportsis just exported as thedefaultexport and the specialdefaultimport syntax gives you easy access tomodule.exports(i.e.const foo = require('bar')is the same asimport foo from 'bar').However, it took a while for ES module syntax to be supported natively by JavaScript runtimes, and people still wanted to start using ES module syntax in the meantime. The Babel JavaScript compiler let you do this. You could transform each ES module file into a CommonJS module file that behaved the same. However, this transformation has a problem: emulating the
importsyntax accurately as described above means thatexport default 0andimport foo from 'bar'will no longer line up when transformed to CommonJS. The codeexport default 0turns intomodule.exports.default = 0and the codeimport foo from 'bar'turns intoconst foo = require('bar'), meaningfoois0before the transformation butfoois{ default: 0 }after the transformation.To fix this, Babel sets the property
__esModuleto true as a signal to itself when it converts an ES module to a CommonJS module. Then, when importing adefaultexport, it can know to use the value ofmodule.exports.defaultinstead ofmodule.exportsto make sure the behavior of the CommonJS modules correctly matches the behavior of the original ES modules. This fix has been widely adopted across the ecosystem and has made it into other tools such as TypeScript and even esbuild.However, when Node.js finally released their ES module implementation, they went with the original implementation where the
defaultexport is alwaysmodule.exports, which broke compatibility with the existing ecosystem of ES modules that had been cross-compiled into CommonJS modules by Babel. You now have to either add or remove an additional.defaultproperty depending on whether your code needs to run in a Node environment or in a Babel environment, which created an interoperability headache. In addition, JavaScript tools such as esbuild now need to guess whether you want Node-style or Babel-styledefaultimports. There's no way for a tool to know with certainty which one a given file is expecting and if your tool guesses wrong, your code will break.This release changes esbuild's heuristics around
defaultexports and the__esModulemarker to attempt to improve compatibility with Webpack and Node, which is what most packages are tuned for. The behavior changes are as follows:Old behavior:
-
If an
importstatement is used to load a CommonJS file and a)module.exportsis an object, b)module.exports.__esModuleis truthy, and c) the propertydefaultexists inmodule.exports, then esbuild would set thedefaultexport tomodule.exports.default(like Babel). Otherwise thedefaultexport was set tomodule.exports(like Node). -
If a
requirecall is used to load an ES module file, the returned module namespace object had the__esModuleproperty set to true. This behaved as if the ES module had been converted to CommonJS via a Babel-compatible transformation. -
The
__esModulemarker could inconsistently appear on module namespace objects (i.e.import * as) when writing pure ESM code. Specifically, if a module namespace object was materialized then the__esModulemarker was present, but if it was optimized away then the__esModulemarker was absent. -
It was not allowed to create an ES module export named
__esModule. This avoided generating code that might break due to the inconsistency mentioned above, and also avoided issues with duplicate definitions of__esModule.
New behavior:
-
If an
importstatement is used to load a CommonJS file and a)module.exportsis an object, b)module.exports.__esModuleis truthy, and c) the file name does not end in either.mjsor.mtsand thepackage.jsonfile does not contain"type": "module", then esbuild will set thedefaultexport tomodule.exports.default(like Babel). Otherwise thedefaultexport is set tomodule.exports(like Node).Note that this means the
defaultexport may now be undefined in situations where it previously wasn't undefined. This matches Webpack's behavior so it should hopefully be more compatible.Also note that this means import behavior now depends on the file extension and on the contents of
package.json. This also matches Webpack's behavior to hopefully improve compatibility. -
If a
requirecall is used to load an ES module file, the returned module namespace object has the__esModuleproperty set totrue. This behaves as if the ES module had been converted to CommonJS via a Babel-compatible transformation. -
If an
importstatement orimport()expression is used to load an ES module, the__esModulemarker should now never be present on the module namespace object. This frees up the__esModuleexport name for use with ES modules. -
It's now allowed to use
__esModuleas a normal export name in an ES module. This property will be accessible to other ES modules but will not be accessible to code that loads the ES module usingrequire, where they will observe the property set totrueinstead.
-
-
Pass the current esbuild instance to JS plugins (#1790)
Previously JS plugins that wanted to run esbuild had to
require('esbuild')to get the esbuild object. However, that could potentially result in a different version of esbuild. This is also more complicated to do outside of node (such as within a browser). With this release, the current esbuild instance is now passed to JS plugins as theesbuildproperty:let examplePlugin = { name: 'example', setup(build) { console.log(build.esbuild.version) console.log(build.esbuild.transformSync('1+2')) }, } -
Disable
calc()transform for results with non-finite numbers (#1839)This release disables minification of
calc()expressions when the result containsNaN,-Infinity, orInfinity. These numbers are valid inside ofcalc()expressions but not outside of them, so thecalc()expression must be preserved in these cases. -
Move
"use strict"before injected shim imports (#1837)If a CommonJS file contains a
"use strict"directive, it could potentially be unintentionally disabled by esbuild when using the "inject" feature when bundling is enabled. This is because the inject feature was inserting a call to the initializer for the injected file before the"use strict"directive. In JavaScript, directives do not apply if they come after a non-directive statement. This release fixes the problem by moving the"use strict"directive before the initializer for the injected file so it isn't accidentally disabled. -
Pass the ignored path query/hash suffix to
onLoadplugins (#1827)The built-in
onResolvehandler that comes with esbuild can strip the query/hash suffix off of a path during path resolution. For example,url("fonts/icons.eot?#iefix")can be resolved to the filefonts/icons.eot. For context, IE8 has a bug where it considers the font face URL to extend to the last)instead of the first). In the example below, IE8 thinks the URL for the font isExample.eot?#iefix') format('eot'), url('Example.ttf') format('truetypeso by adding?#iefix, IE8 thinks the URL has a path ofExample.eotand a query string of?#iefix') format('eot...and can load the font file:@font-face { font-family: 'Example'; src: url('Example.eot?#iefix') format('eot'), url('Example.ttf') format('truetype'); }However, the suffix is not currently passed to esbuild and plugins may want to use this suffix for something. Previously plugins had to add their own
onResolvehandler if they wanted to use the query suffix. With this release, the suffix can now be returned by plugins fromonResolveand is now passed to plugins inonLoad:let examplePlugin = { name: 'example', setup(build) { build.onResolve({ filter: /.*/ }, args => { return { path: args.path, suffix: '?#iefix' } }) build.onLoad({ filter: /.*/ }, args => { console.log({ path: args.path, suffix: args.suffix }) }) }, }The suffix is deliberately not included in the path that's provided to plugins because most plugins won't know to handle this strange edge case and would likely break. Keeping the suffix out of the path means that plugins can opt-in to handling this edge case if they want to, and plugins that aren't aware of this edge case will likely still do something reasonable.
-
Add
[ext]placeholder for path templates (#1799)This release adds the
[ext]placeholder to the--entry-names=,--chunk-names=, and--asset-names=configuration options. The[ext]placeholder takes the value of the file extension without the leading., and can be used to place output files with different file extensions into different folders. For example,--asset-names=assets/[ext]/[name]-[hash]might generate an output path ofassets/png/image-LSAMBFUD.png.This feature was contributed by @LukeSheard.
-
Disable star-to-clause transform for external imports (#1801)
When bundling is enabled, esbuild automatically transforms
import * as x from 'y'; x.z()intoimport {z} as 'y'; z()to improve tree shaking. This avoids needing to create the import namespace objectxif it's unnecessary, which can result in the removal of large amounts of unused code. However, this transform shouldn't be done for external imports because that incorrectly changes the semantics of the import. If the exportzdoesn't exist in the previous example, the valuex.zis a property access that is undefined at run-time, but the valuezis an import error that will prevent the code from running entirely. This release fixes the problem by avoiding doing this transform for external imports:// Original code import * as x from 'y'; x.z(); // Old output (with --bundle --format=esm --external:y) import { z } from "y"; z(); // New output (with --bundle --format=esm --external:y) import * as x from "y"; x.z(); -
Disable
calc()transform for numbers with many fractional digits (#1821)Version 0.13.12 introduced simplification of
calc()expressions in CSS when minifying. For example,calc(100% / 4)turns into25%. However, this is problematic for numbers with many fractional digits because either the number is printed with reduced precision, which is inaccurate, or the number is printed with full precision, which could be longer than the original expression. For example, turningcalc(100% / 3)into33.33333%is inaccurate and turning it into33.333333333333336%likely isn't desired. In this release, minification ofcalc()is now disabled when any number in the result cannot be represented to full precision with at most five fractional digits. -
Fix an edge case with
catchscope handling (#1812)This release fixes a subtle edge case with
catchscope and destructuring assignment. Identifiers in computed properties and/or default values inside the destructuring binding pattern should reference the outer scope, not the inner scope. The fix was to split the destructuring pattern into its own scope, separate from thecatchbody. Here's an example of code that was affected by this edge case:// Original code let foo = 1 try { throw ['a', 'b'] } catch ({ [foo]: y }) { let foo = 2 assert(y === 'b') } // Old output (with --minify) let foo=1;try{throw["a","b"]}catch({[o]:t}){let o=2;assert(t==="b")} // New output (with --minify) let foo=1;try{throw["a","b"]}catch({[foo]:t}){let o=2;assert(t==="b")} -
Go 1.17.2 was upgraded to Go 1.17.4
The previous release was built with Go 1.17.2, but this release is built with Go 1.17.4. This is just a routine upgrade. There are no changes significant to esbuild outside of some security-related fixes to Go's HTTP stack (but you shouldn't be running esbuild's dev server in production anyway).
One notable change related to this is that esbuild's publishing script now ensures that git's state is free of uncommitted and/or untracked files before building. Previously this wasn't the case because publishing esbuild involved changing the version number, running the publishing script, and committing at the end, which meant that files were uncommitted during the build process. I also typically had some untracked test files in the same directory during publishing (which is harmless).
This matters because there's an upcoming change in Go 1.18 where the Go compiler will include metadata about whether there are untracked files or not when doing a build: https://github.com/golang/go/issues/37475. Changing esbuild's publishing script should mean that when esbuild upgrades to Go 1.18, esbuild's binary executables will be marked as being built off of a specific commit without any modifications. This is important for reproducibility. Checking out a specific esbuild commit and building it should give a bitwise-identical binary executable to one that I published. But if this metadata indicated that there were untracked files during the published build, then the resulting executable would no longer be bitwise-identical.
-
Fix
importsinpackage.json(#1807)This release contains a fix for the rarely-used
importsfeature inpackage.jsonfiles that lets a package specify a custom remapping for import paths inside that package that start with#. Support forimportswas added in version 0.13.9. However, the field was being incorrectly interpreted as relative to the importing file instead of to thepackage.jsonfile, which caused an import failure when the importing file is in a subdirectory instead of being at the top level of the package. Import paths should now be interpreted as relative to the correct directory which should fix these path resolution failures. -
Isolate implicit sibling scope lookup for
enumandnamespaceThe previous release implemented sibling namespaces in TypeScript, which introduces a new kind of scope lookup that doesn't exist in JavaScript. Exported members inside an
enumornamespaceblock can be implicitly referenced in a siblingenumornamespaceblock just by using the name without using a property reference. However, this behavior appears to only work forenum-to-enumandnamespace-to-namespaceinteractions. Even though sibling enums and namespaces with the same name can be merged together into the same underlying object, this implicit reference behavior doesn't work forenum-to-namespaceinteractions and attempting to do this with anamespace-to-enuminteraction causes the TypeScript compiler itself to crash. Here is an example of how the TypeScript compiler behaves in each case:// "b" is accessible enum a { b = 1 } enum a { c = b } // "e" is accessible namespace d { export let e = 1 } namespace d { export let f = e } // "h" is inaccessible enum g { h = 1 } namespace g { export let i = h } // This causes the TypeScript compiler to crash namespace j { export let k = 1 } enum j { l = k }This release changes the implicit sibling scope lookup behavior to only work for
enum-to-enumandnamespace-to-namespaceinteractions. These implicit references no longer work withenum-to-namespaceandnamespace-to-enuminteractions, which should more accurately match the behavior of the TypeScript compiler. -
Add semicolon insertion before TypeScript-specific definite assignment assertion modifier (#1810)
TypeScript lets you add a
!after a variable declaration to bypass TypeScript's definite assignment analysis:let x!: number[]; initialize(); x.push(4); function initialize() { x = [0, 1, 2, 3]; }This
!is called a definite assignment assertion and tells TypeScript to assume that the variable has been initialized somehow. However, JavaScript's automatic semicolon insertion rules should be able to insert a semicolon before it:let a !function(){}()Previously the above code was incorrectly considered a syntax error in TypeScript. With this release, this code is now parsed correctly.
-
Log output to stderr has been overhauled
This release changes the way log messages are formatted to stderr. The changes make the kind of message (e.g. error vs. warning vs. note) more obvious, and they also give more room for paragraph-style notes that can provide more detail about the message. Here's an example:
Before:
> example.tsx:14:25: warning: Comparison with -0 using the "===" operator will also match 0 14 │ case 1: return x === -0 ╵ ~~ > example.tsx:21:23: error: Could not resolve "path" (use "--platform=node" when building for node) 21 │ const path = require('path') ╵ ~~~~~~After:
▲ [WARNING] Comparison with -0 using the "===" operator will also match 0 example.tsx:14:25: 14 │ case 1: return x === -0 ╵ ~~ Floating-point equality is defined such that 0 and -0 are equal, so "x === -0" returns true for both 0 and -0. You need to use "Object.is(x, -0)" instead to test for -0. ✘ [ERROR] Could not resolve "path" example.tsx:21:23: 21 │ const path = require('path') ╵ ~~~~~~ The package "path" wasn't found on the file system but is built into node. Are you trying to bundle for node? You can use "--platform=node" to do that, which will remove this error.Note that esbuild's formatted log output is for humans, not for machines. If you need to output a stable machine-readable format, you should be using the API for that. Build and transform results have arrays called
errorsandwarningswith objects that represent the log messages. -
Show inlined enum value names in comments
When esbuild inlines an enum, it will now put a comment next to it with the original enum name:
// Original code const enum Foo { FOO } console.log(Foo.FOO) // Old output console.log(0); // New output console.log(0 /* FOO */);This matches the behavior of the TypeScript compiler, and should help with debugging. These comments are not generated if minification is enabled.
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.13.0. See the documentation about semver for more information.
-
Add support for TypeScript's
preserveValueImportssetting (#1525)TypeScript 4.5, which was just released, added a new setting called
preserveValueImports. This release of esbuild implements support for this new setting. However, this release also changes esbuild's behavior regarding theimportsNotUsedAsValuessetting, so this release is being considered a breaking change. Now esbuild's behavior should more accurately match the behavior of the TypeScript compiler. This is described in more detail below.The difference in behavior is around unused imports. By default, unused import names are considered to be types and are completely removed if they are unused. If all import names are removed for a given import statement, then the whole import statement is removed too. The two
tsconfig.jsonsettingsimportsNotUsedAsValuesandpreserveValueImportslet you customize this. Here's what the TypeScript compiler's output looks like with these different settings enabled:// Original code import { unused } from "foo"; // Default output /* (the import is completely removed) */ // Output with "importsNotUsedAsValues": "preserve" import "foo"; // Output with "preserveValueImports": true import { unused } from "foo";Previously, since the
preserveValueImportssetting didn't exist yet, esbuild had treated theimportsNotUsedAsValuessetting as if it were what is now thepreserveValueImportssetting instead. This was a deliberate deviation from how the TypeScript compiler behaves, but was necessary to allow esbuild to be used as a TypeScript-to-JavaScript compiler inside of certain composite languages such as Svelte and Vue. These languages append additional code after converting the TypeScript to JavaScript so unused imports may actually turn out to be used later on:<script> import { someFunc } from "./some-module.js"; </script> <button on:click={someFunc}>Click me!</button>Previously the implementers of these languages had to use the
importsNotUsedAsValuessetting as a hack for esbuild to preserve the import statements. With this release, esbuild now follows the behavior of the TypeScript compiler so implementers will need to use the newpreserveValueImportssetting to do this instead. This is the breaking change. -
TypeScript code follows JavaScript class field semantics with
--target=esnext(#1480)TypeScript 4.3 included a subtle breaking change that wasn't mentioned in the TypeScript 4.3 blog post: class fields will now be compiled with different semantics if
"target": "ESNext"is present intsconfig.json. Specifically in this caseuseDefineForClassFieldswill default totruewhen not specified instead offalse. This means class field behavior in TypeScript code will now match JavaScript instead of doing something else:class Base { set foo(value) { console.log('set', value) } } class Derived extends Base { foo = 123 } new Derived()In TypeScript 4.2 and below, the TypeScript compiler would generate code that prints
set 123whentsconfig.jsoncontains"target": "ESNext"but in TypeScript 4.3 and above, the TypeScript compiler will now generate code that doesn't print anything. This is the difference between "assign" semantics and "define" semantics.Previously you had to create a
tsconfig.jsonfile and specify"target": "ESNext"to get this behavior in esbuild. With this release, you can now also just pass--target=esnextto esbuild to force-enable this behavior. Note that esbuild doesn't do this by default even though the default value of--target=otherwise behaves likeesnext. Since TypeScript's compiler doesn't do this behavior by default, it seems like a good idea for esbuild to not do this behavior by default either.
In addition to the breaking changes above, the following changes are also included in this release:
-
Allow certain keywords as tuple type labels in TypeScript (#1797)
Apparently TypeScript lets you use certain keywords as tuple labels but not others. For example,
type x = [function: number]is allowed whiletype x = [class: number]isn't. This release replicates this behavior in esbuild's TypeScript parser:-
Allowed keywords:
false,function,import,new,null,this,true,typeof,void -
Forbidden keywords:
break,case,catch,class,const,continue,debugger,default,delete,do,else,enum,export,extends,finally,for,if,in,instanceof,return,super,switch,throw,try,var,while,with
-
-
Support sibling namespaces in TypeScript (#1410)
TypeScript has a feature where sibling namespaces with the same name can implicitly reference each other's exports without an explicit property access. This goes against how scope lookup works in JavaScript, so it previously didn't work with esbuild. This release adds support for this feature:
// Original TypeScript code namespace x { export let y = 123 } namespace x { export let z = y } // Old JavaScript output var x; (function(x2) { x2.y = 123; })(x || (x = {})); (function(x2) { x2.z = y; })(x || (x = {})); // New JavaScript output var x; (function(x2) { x2.y = 123; })(x || (x = {})); (function(x2) { x2.z = x2.y; })(x || (x = {}));Notice how the identifier
yis now compiled to the property accessx2.ywhich references the export namedyon the namespace, instead of being left as the identifierywhich references the global namedy. This matches how the TypeScript compiler treats namespace objects. This new behavior also works for enums:// Original TypeScript code enum x { y = 123 } enum x { z = y + 1 } // Old JavaScript output var x; (function(x2) { x2[x2["y"] = 123] = "y"; })(x || (x = {})); (function(x2) { x2[x2["z"] = y + 1] = "z"; })(x || (x = {})); // New JavaScript output var x; (function(x2) { x2[x2["y"] = 123] = "y"; })(x || (x = {})); (function(x2) { x2[x2["z"] = 124] = "z"; })(x || (x = {}));Note that this behavior does not work across files. Each file is still compiled independently so the namespaces in each file are still resolved independently per-file. Implicit namespace cross-references still do not work across files. Getting this to work is counter to esbuild's parallel architecture and does not fit in with esbuild's design. It also doesn't make sense with esbuild's bundling model where input files are either in ESM or CommonJS format and therefore each have their own scope.
-
Change output for top-level TypeScript enums
The output format for top-level TypeScript enums has been changed to reduce code size and improve tree shaking, which means that esbuild's enum output is now somewhat different than TypeScript's enum output. The behavior of both output formats should still be equivalent though. Here's an example that shows the difference:
// Original code enum x { y = 1, z = 2 } // Old output var x; (function(x2) { x2[x2["y"] = 1] = "y"; x2[x2["z"] = 2] = "z"; })(x || (x = {})); // New output var x = /* @__PURE__ */ ((x2) => { x2[x2["y"] = 1] = "y"; x2[x2["z"] = 2] = "z"; return x2; })(x || {});The function expression has been changed to an arrow expression to reduce code size and the enum initializer has been moved into the variable declaration to make it possible to be marked as
/* @__PURE__ */to improve tree shaking. The/* @__PURE__ */annotation is now automatically added when all of the enum values are side-effect free, which means the entire enum definition can be removed as dead code if it's never referenced. Direct enum value references within the same file that have been inlined do not count as references to the enum definition so this should eliminate enums from the output in many cases:// Original code enum Foo { FOO = 1 } enum Bar { BAR = 2 } console.log(Foo, Bar.BAR) // Old output (with --bundle --minify) var n;(function(e){e[e.FOO=1]="FOO"})(n||(n={}));var l;(function(e){e[e.BAR=2]="BAR"})(l||(l={}));console.log(n,2); // New output (with --bundle --minify) var n=(e=>(e[e.FOO=1]="FOO",e))(n||{});console.log(n,2);Notice how the new output is much shorter because the entire definition for
Barhas been completely removed as dead code by esbuild's tree shaking.The output may seem strange since it would be simpler to just have a plain object literal as an initializer. However, TypeScript's enum feature behaves similarly to TypeScript's namespace feature which means enums can merge with existing enums and/or existing namespaces (and in some cases also existing objects) if the existing definition has the same name. This new output format keeps its similarity to the original output format so that it still handles all of the various edge cases that TypeScript's enum feature supports. Initializing the enum using a plain object literal would not merge with existing definitions and would break TypeScript's enum semantics.
-
Fix legal comment parsing in CSS (#1796)
Legal comments in CSS either start with
/*!or contain@preserveor@licenseand are preserved by esbuild in the generated CSS output. This release fixes a bug where non-top-level legal comments inside a CSS file caused esbuild to skip any following legal comments even if those following comments are top-level:/* Original code */ .example { --some-var: var(--tw-empty, /*!*/ /*!*/); } /*! Some legal comment */ body { background-color: red; } /* Old output (with --minify) */ .example{--some-var: var(--tw-empty, )}body{background-color:red} /* New output (with --minify) */ .example{--some-var: var(--tw-empty, )}/*! Some legal comment */body{background-color:red} -
Fix panic when printing invalid CSS (#1803)
This release fixes a panic caused by a conditional CSS
@importrule with a URL token. Code like this caused esbuild to enter an unexpected state because the case where tokens in the import condition with associated import records wasn't handled. This case is now handled correctly:@import "example.css" url(foo); -
Mark
SetandMapwith array arguments as pure (#1791)This release introduces special behavior for references to the global
SetandMapconstructors that marks them as/* @__PURE__ */if they are known to not have any side effects. These constructors evaluate the iterator of whatever is passed to them and the iterator could have side effects, so this is only safe if whatever is passed to them is an array, since the array iterator has no side effects.Marking a constructor call as
/* @__PURE__ */means it's safe to remove if the result is unused. This is an existing feature that you can trigger by manually adding a/* @__PURE__ */comment before a constructor call. The difference is that this release contains special behavior to automatically markSetandMapas pure for you as long as it's safe to do so. As with all constructor calls that are marked/* @__PURE__ */, any internal expressions which could cause side effects are still preserved even though the constructor call itself is removed:// Original code new Map([ ['a', b()], [c(), new Set(['d', e()])], ]); // Old output (with --minify) new Map([["a",b()],[c(),new Set(["d",e()])]]); // New output (with --minify) b(),c(),e();
-
Fix
superin loweredasyncarrow functions (#1777)This release fixes an edge case that was missed when lowering
asyncarrow functions containingsuperproperty accesses for compile targets that don't supportasyncsuch as with--target=es6. The problem was that lowering transformsasyncarrow functions into generator function expressions that are then passed to an esbuild helper function called__asyncthat implements theasyncstate machine behavior. Since function expressions do not capturethisandsuperlike arrow functions do, this led to a mismatch in behavior which meant that the transform was incorrect. The fix is to introduce a helper function to forwardsuperaccess into the generator function expression body. Here's an example:// Original code class Foo extends Bar { foo() { return async () => super.bar() } } // Old output (with --target=es6) class Foo extends Bar { foo() { return () => __async(this, null, function* () { return super.bar(); }); } } // New output (with --target=es6) class Foo extends Bar { foo() { return () => { var __superGet = (key) => super[key]; return __async(this, null, function* () { return __superGet("bar").call(this); }); }; } } -
Avoid merging certain CSS rules with different units (#1732)
This release no longer collapses
border-radius,margin,padding, andinsetrules when they have units with different levels of browser support. Collapsing multiple of these rules into a single rule is not equivalent if the browser supports one unit but not the other unit, since one rule would still have applied before the collapse but no longer applies after the collapse due to the whole rule being ignored. For example, Chrome 10 supports theremunit but not thevwunit, so the CSS code below should render with rounded corners in Chrome 10. However, esbuild previously merged everything into a single rule which would cause Chrome 10 to ignore the rule and not round the corners. This issue is now fixed:/* Original CSS */ div { border-radius: 1rem; border-top-left-radius: 1vw; margin: 0; margin-top: 1Q; left: 10Q; top: 20Q; right: 10Q; bottom: 20Q; } /* Old output (with --minify) */ div{border-radius:1vw 1rem 1rem;margin:1Q 0 0;inset:20Q 10Q} /* New output (with --minify) */ div{border-radius:1rem;border-top-left-radius:1vw;margin:0;margin-top:1Q;inset:20Q 10Q}Notice how esbuild can still collapse rules together when they all share the same unit, even if the unit is one that doesn't have universal browser support such as the unit
Q. One subtlety is that esbuild now distinguishes between "safe" and "unsafe" units where safe units are old enough that they are guaranteed to work in any browser a user might reasonably use, such aspx. Safe units are allowed to be collapsed together even if there are multiple different units while multiple different unsafe units are not allowed to be collapsed together. Another detail is that esbuild no longer minifies zero lengths by removing the unit if the unit is unsafe (e.g.0reminto0) since that could cause a rendering difference if a previously-ignored rule is now no longer ignored due to the unit change. If you are curious, you can learn more about browser support levels for different CSS units in Mozilla's documentation about CSS length units. -
Avoid warning about ignored side-effect free imports for empty files (#1785)
When bundling, esbuild warns about bare imports such as
import "lodash-es"when the package has been marked as"sideEffects": falsein itspackage.jsonfile. This is because the only reason to use a bare import is because you are relying on the side effects of the import, but imports for packages marked as side-effect free are supposed to be removed. If the package indicates that it has no side effects, then this bare import is likely a bug.However, some people have packages just for TypeScript type definitions. These package can actually have a side effect as they can augment the type of the global object in TypeScript, even if they are marked with
"sideEffects": false. To avoid warning in this case, esbuild will now only issue this warning if the imported file is non-empty. If the file is empty, then it's irrelevant whether you import it or not so any import of that file does not indicate a bug. This fixes this case because.d.tsfiles typically end up being empty after esbuild parses them since they typically only contain type declarations. -
Attempt to fix packages broken due to the
node:prefix (#1760)Some people have started using the node-specific
node:path prefix in their packages. This prefix forces the following path to be interpreted as a node built-in module instead of a package on the file system. Sorequire("node:path")will always import node'spathmodule and never import npm'spathpackage.Adding the
node:prefix breaks that code with older node versions that don't understand thenode:prefix. This is a problem with the package, not with esbuild. The package should be adding a fallback if thenode:prefix isn't available. However, people still want to be able to use these packages with older node versions even though the code is broken. Now esbuild will automatically strip this prefix if it detects that the code will break in the configured target environment (as specified by--target=). Note that this only happens during bundling, since import paths are only examined during bundling.
-
Fix dynamic
import()on node 12.20+ (#1772)When you use flags such as
--target=node12.20, esbuild uses that version number to see what features the target environment supports. This consults an internal table that stores which target environments are supported for each feature. For example,import(x)is changed intoPromise.resolve().then(() => require(x))if dynamicimportexpressions are unsupported.Previously esbuild's internal table only stored one version number, since features are rarely ever removed in newer versions of software. Either the target environment is before that version and the feature is unsupported, or the target environment is after that version and the feature is supported. This approach has work for all relevant features in all cases except for one: dynamic
importsupport in node. This feature is supported in node 12.20.0 up to but not including node 13.0.0, and then is also supported in node 13.2.0 up. The feature table implementation has been changed to store an array of potentially discontiguous version ranges instead of one version number.Up until now, esbuild used 13.2.0 as the lowest supported version number to avoid generating dynamic
importexpressions when targeting node versions that don't support it. But with this release, esbuild will now use the more accurate discontiguous version range in this case. This means dynamicimportexpressions can now be generated when targeting versions of node 12.20.0 up to but not including node 13.0.0. -
Avoid merging certain qualified rules in CSS (#1776)
A change was introduced in the previous release to merge adjacent CSS rules that have the same content:
/* Original code */ a { color: red } b { color: red } /* Minified output */ a,b{color:red}However, that introduced a regression in cases where the browser considers one selector to be valid and the other selector to be invalid, such as in the following example:
/* This rule is valid, and is applied */ a { color: red } /* This rule is invalid, and is ignored */ b:-x-invalid { color: red }Merging these two rules into one causes the browser to consider the entire merged rule to be invalid, which disables both rules. This is a change in behavior from the original code.
With this release, esbuild will now only merge adjacent duplicate rules together if they are known to work in all browsers (specifically, if they are known to work in IE 7 and up). Adjacent duplicate rules will no longer be merged in all other cases including modern pseudo-class selectors such as
:focus, HTML5 elements such asvideo, and combinators such asa + b. -
Minify syntax in the CSS
font,font-family, andfont-weightproperties (#1756)This release includes size reductions for CSS font syntax when minification is enabled:
/* Original code */ div { font: bold 1rem / 1.2 "Segoe UI", sans-serif, "Segoe UI Emoji"; } /* Output with "--minify" */ div{font:700 1rem/1.2 Segoe UI,sans-serif,"Segoe UI Emoji"}Notice how
boldhas been changed to700and the quotes were removed around"Segoe UI"since it was safe to do so.This feature was contributed by @sapphi-red.
-
Add more information about skipping
"main"inpackage.json(#1754)Configuring
mainFields: []breaks most npm packages since it tells esbuild to ignore the"main"field inpackage.json, which most npm packages use to specify their entry point. This is not a bug with esbuild because esbuild is just doing what it was told to do. However, people may do this without understanding how npm packages work, and then be confused about why it doesn't work. This release now includes additional information in the error message:> foo.js:1:27: error: Could not resolve "events" (use "--platform=node" when building for node) 1 │ var EventEmitter = require('events') ╵ ~~~~~~~~ node_modules/events/package.json:20:2: note: The "main" field was ignored because the list of main fields to use is currently set to [] 20 │ "main": "./events.js", ╵ ~~~~~~ -
Fix a tree-shaking bug with
var exports(#1739)This release fixes a bug where a variable named
var exports = {}was incorrectly removed by tree-shaking (i.e. dead code elimination). Theexportsvariable is a special variable in CommonJS modules that is automatically provided by the CommonJS runtime. CommonJS modules are transformed into something like this before being run:function(exports, module, require) { var exports = {} }So using
var exports = {}should have the same effect asexports = {}because the variableexportsshould already be defined. However, esbuild was incorrectly overwriting the definition of theexportsvariable with the one provided by CommonJS. This release merges the definitions together so both are included, which fixes the bug. -
Merge adjacent CSS selector rules with duplicate content (#1755)
With this release, esbuild will now merge adjacent selectors when minifying if they have the same content:
/* Original code */ a { color: red } b { color: red } /* Old output (with --minify) */ a{color:red}b{color:red} /* New output (with --minify) */ a,b{color:red} -
Shorten
top,right,bottom,leftCSS property intoinsetwhen it is supported (#1758)This release enables collapsing of
insetrelated properties:/* Original code */ div { top: 0; right: 0; bottom: 0; left: 0; } /* Output with "--minify-syntax" */ div { inset: 0; }This minification rule is only enabled when
insetproperty is supported by the target environment. Make sure to set esbuild'stargetsetting correctly when minifying if the code will be running in an older environment (e.g. earlier than Chrome 87).This feature was contributed by @sapphi-red.
-
Implement initial support for simplifying
calc()expressions in CSS (#1607)This release includes basic simplification of
calc()expressions in CSS when minification is enabled. The approach mainly follows the official CSS specification, which means it should behave the way browsers behave: https://www.w3.org/TR/css-values-4/#calc-func. This is a basic implementation so there are probably somecalc()expressions that can be reduced by other tools but not by esbuild. This release mainly focuses on setting up the parsing infrastructure forcalc()expressions to make it straightforward to implement additional simplifications in the future. Here's an example of this new functionality:/* Input CSS */ div { width: calc(60px * 4 - 5px * 2); height: calc(100% / 4); } /* Output CSS (with --minify-syntax) */ div { width: 230px; height: 25%; }Expressions that can't be fully simplified will still be partially simplified into a reduced
calc()expression:/* Input CSS */ div { width: calc(100% / 5 - 2 * 1em - 2 * 1px); } /* Output CSS (with --minify-syntax) */ div { width: calc(20% - 2em - 2px); }Note that this transformation doesn't attempt to modify any expression containing a
var()CSS variable reference. These variable references can contain any number of tokens so it's not safe to move forward with a simplification assuming thatvar()is a single token. For example,calc(2px * var(--x) * 3)is not transformed intocalc(6px * var(--x))in casevar(--x)contains something like4 + 5px(calc(2px * 4 + 5px * 3)evaluates to23pxwhilecalc(6px * 4 + 5px)evaluates to29px). -
Fix a crash with a legal comment followed by an import (#1730)
Version 0.13.10 introduced parsing for CSS legal comments but caused a regression in the code that checks whether there are any rules that come before
@import. This is not desired because browsers ignore@importrules after other non-@importrules, so esbuild warns you when you do this. However, legal comments are modeled as rules in esbuild's internal AST even though they aren't actual CSS rules, and the code that performs this check wasn't updated. This release fixes the crash.
-
Implement class static blocks (#1558)
This release adds support for a new upcoming JavaScript feature called class static blocks that lets you evaluate code inside of a class body. It looks like this:
class Foo { static { this.foo = 123 } }This can be useful when you want to use
try/catchor access private#namefields during class initialization. Doing that without this feature is quite hacky and basically involves creating temporary static fields containing immediately-invoked functions and then deleting the fields after class initialization. Static blocks are much more ergonomic and avoid performance loss due todeletechanging the object shape.Static blocks are transformed for older browsers by moving the static block outside of the class body and into an immediately invoked arrow function after the class definition:
// The transformed version of the example code above const _Foo = class { }; let Foo = _Foo; (() => { _Foo.foo = 123; })();In case you're wondering, the additional
letvariable is to guard against the potential reassignment ofFooduring evaluation such as what happens below. The value ofthismust be bound to the original class, not to the current value ofFoo:let bar class Foo { static { bar = () => this } } Foo = null console.log(bar()) // This should not be "null" -
Fix issues with
superproperty accessesCode containing
superproperty accesses may need to be transformed even when they are supported. For example, in ES6asyncmethods are unsupported whilesuperproperties are supported. Anasyncmethod containingsuperproperty accesses requires those uses ofsuperto be transformed (theasyncfunction is transformed into a nested generator function and thesuperkeyword cannot be used inside nested functions).Previously esbuild transformed
superproperty accesses into a function call that returned the corresponding property. However, this was incorrect for uses ofsuperthat write to the inherited setter since a function call is not a valid assignment target. This release fixes writing to asuperproperty:// Original code class Base { set foo(x) { console.log('set foo to', x) } } class Derived extends Base { async bar() { super.foo = 123 } } new Derived().bar() // Old output with --target=es6 (contains a syntax error) class Base { set foo(x) { console.log("set foo to", x); } } class Derived extends Base { bar() { var __super = (key) => super[key]; return __async(this, null, function* () { __super("foo") = 123; }); } } new Derived().bar(); // New output with --target=es6 (works correctly) class Base { set foo(x) { console.log("set foo to", x); } } class Derived extends Base { bar() { var __superSet = (key, value) => super[key] = value; return __async(this, null, function* () { __superSet("foo", 123); }); } } new Derived().bar();All known edge cases for assignment to a
superproperty should now be covered including destructuring assignment and using the unary assignment operators with BigInts.In addition, this release also fixes a bug where a
staticclass field containing asuperproperty access was not transformed when it was moved outside of the class body, which can happen whenstaticclass fields aren't supported.// Original code class Base { static get foo() { return 123 } } class Derived extends Base { static bar = super.foo } // Old output with --target=es6 (contains a syntax error) class Base { static get foo() { return 123; } } class Derived extends Base { } __publicField(Derived, "bar", super.foo); // New output with --target=es6 (works correctly) class Base { static get foo() { return 123; } } const _Derived = class extends Base { }; let Derived = _Derived; __publicField(Derived, "bar", __superStaticGet(_Derived, "foo"));All known edge cases for
superinsidestaticclass fields should be handled including accessingsuperafter prototype reassignment of the enclosing class object.
-
Implement legal comment preservation for CSS (#1539)
This release adds support for legal comments in CSS the same way they are already supported for JS. A legal comment is one that starts with
/*!or that contains the text@licenseor@preserve. These comments are preserved in output files by esbuild since that follows the intent of the original authors of the code. The specific behavior is controlled via--legal-comments=in the CLI andlegalCommentsin the JS API, which can be set to any of the following options:none: Do not preserve any legal commentsinline: Preserve all rule-level legal commentseof: Move all rule-level legal comments to the end of the filelinked: Move all rule-level legal comments to a.LEGAL.txtfile and link to them with a commentexternal: Move all rule-level legal comments to a.LEGAL.txtfile but to not link to them
The default behavior is
eofwhen bundling andinlineotherwise. -
Allow uppercase
es*targets (#1717)With this release, you can now use target names such as
ESNextinstead ofesnextas the target name in the CLI and JS API. This is important because people don't want to have to call.toLowerCase()on target strings from TypeScript'stsconfig.jsonfile before passing it to esbuild (TypeScript uses case-agnostic target names).This feature was contributed by @timse.
-
Update to Unicode 14.0.0
The character tables that determine which characters form valid JavaScript identifiers have been updated from Unicode version 13.0.0 to the newly-released Unicode version 14.0.0. I'm not putting an example in the release notes because all of the new characters will likely just show up as little squares since fonts haven't been updated yet. But you can read https://www.unicode.org/versions/Unicode14.0.0/#Summary for more information about the changes.
-
Add support for
importsinpackage.json(#1691)This release adds basic support for the
importsfield inpackage.json. It behaves similarly to theexportsfield but only applies to import paths that start with#. Theimportsfield provides a way for a package to remap its own internal imports for itself, while theexportsfield provides a way for a package to remap its external exports for other packages. This is useful because theimportsfield respects the currently-configured conditions which means that the import mapping can change at run-time. For example:$ cat entry.mjs import '#example' $ cat package.json { "imports": { "#example": { "foo": "./example.foo.mjs", "default": "./example.mjs" } } } $ cat example.foo.mjs console.log('foo is enabled') $ cat example.mjs console.log('foo is disabled') $ node entry.mjs foo is disabled $ node --conditions=foo entry.mjs foo is enabledNow that esbuild supports this feature too, import paths starting with
#and any provided conditions will be respected when bundling:$ esbuild --bundle entry.mjs | node foo is disabled $ esbuild --conditions=foo --bundle entry.mjs | node foo is enabled -
Fix using
npm rebuildwith theesbuildpackage (#1703)Version 0.13.4 accidentally introduced a regression in the install script where running
npm rebuildmultiple times could fail after the second time. The install script creates a copy of the binary executable usinglinkfollowed byrename. Usinglinkcreates a hard link which saves space on the file system, andrenameis used for safety since it atomically replaces the destination.However, the
renamesyscall has an edge case where it silently fails if the source and destination are both the same link. This meant that the install script would fail after being run twice in a row. With this release, the install script now deletes the source after callingrenamein case it has silently failed, so this issue should now be fixed. It should now be safe to usenpm rebuildwith theesbuildpackage. -
Fix invalid CSS minification of
border-radius(#1702)CSS minification does collapsing of
border-radiusrelated properties. For example:/* Original CSS */ div { border-radius: 1px; border-top-left-radius: 5px; } /* Minified CSS */ div{border-radius:5px 1px 1px}However, this only works for numeric tokens, not identifiers. For example:
/* Original CSS */ div { border-radius: 1px; border-top-left-radius: inherit; } /* Minified CSS */ div{border-radius:1px;border-top-left-radius:inherit}Transforming this to
div{border-radius:inherit 1px 1px}, as was done in previous releases of esbuild, is an invalid transformation and results in incorrect CSS. This release of esbuild fixes this CSS transformation bug.
-
Fix
superinside arrow function inside loweredasyncfunction (#1425)When an
asyncfunction is transformed into a regular function for target environments that don't supportasyncsuch as--target=es6, references tosuperinside that function must be transformed too since theasync-to-regular function transformation moves the function body into a nested function, so thesuperreferences are no longer syntactically valid. However, this transform didn't handle an edge case andsuperreferences inside of an arrow function were overlooked. This release fixes this bug:// Original code class Foo extends Bar { async foo() { return () => super.foo() } } // Old output (with --target=es6) class Foo extends Bar { foo() { return __async(this, null, function* () { return () => super.foo(); }); } } // New output (with --target=es6) class Foo extends Bar { foo() { var __super = (key) => super[key]; return __async(this, null, function* () { return () => __super("foo").call(this); }); } } -
Remove the implicit
/after[dir]in entry names (#1661)The "entry names" feature lets you customize the way output file names are generated. The
[dir]and[name]placeholders are filled in with the directory name and file name of the corresponding entry point file, respectively.Previously
--entry-names=[dir]/[name]and--entry-names=[dir][name]behaved the same because the value used for[dir]always had an implicit trailing slash, since it represents a directory. However, some people want to be able to remove the file name with--entry-names=[dir]and the implicit trailing slash gets in the way.With this release, you can now use the
[dir]placeholder without an implicit trailing slash getting in the way. For example, the commandesbuild foo/bar/index.js --outbase=. --outdir=out --entry-names=[dir]previously generated the fileout/foo/bar/.jsbut will now generate the fileout/foo/bar.js.
-
Minify CSS alpha values correctly (#1682)
When esbuild uses the
rgba()syntax for a color instead of the 8-character hex code (e.g. whentargetis set to Chrome 61 or earlier), the 0-to-255 integer alpha value must be printed as a floating-point fraction between 0 and 1. The fraction was only printed to three decimal places since that is the minimal number of decimal places required for all 256 different alpha values to be uniquely determined. However, using three decimal places does not necessarily result in the shortest result. For example,128 / 255is0.5019607843137255which is printed as".502"using three decimal places, but".5"is equivalent becauseround(0.5 * 255) == 128, so printing".5"would be better. With this release, esbuild will always use the minimal numeric representation for the alpha value:/* Original code */ a { color: #FF800080 } /* Old output (with --minify --target=chrome61) */ a{color:rgba(255,128,0,.502)} /* New output (with --minify --target=chrome61) */ a{color:rgba(255,128,0,.5)} -
Match node's behavior for core module detection (#1680)
Node has a hard-coded list of core modules (e.g.
fs) that, when required, short-circuit the module resolution algorithm and instead return the corresponding internal core module object. When you pass--platform=nodeto esbuild, esbuild also implements this short-circuiting behavior and doesn't try to bundle these import paths. This was implemented in esbuild using the existingexternalfeature (e.g. essentially--external:fs). However, there is an edge case where esbuild'sexternalfeature behaved differently than node.Modules specified via esbuild's
externalfeature also cause all sub-paths to be excluded as well, so for example--external:fooexcludes bothfooandfoo/barfrom the bundle. However, node's core module check is only an exact equality check, so for examplefsis a core module and bypasses the module resolution algorithm butfs/foois not a core module and causes the module resolution algorithm to search the file system.This behavior can be used to load a module on the file system with the same name as one of node's core modules. For example,
require('fs/')will load the modulefsfrom the file system instead of loading node's corefsmodule. With this release, esbuild will now match node's behavior in this edge case. This means the external modules that are automatically added by--platform=nodenow behave subtly differently than--external:, which allows code that relies on this behavior to be bundled correctly. -
Fix WebAssembly builds on Go 1.17.2+ (#1684)
Go 1.17.2 introduces a change (specifically a fix for CVE-2021-38297) that causes Go's WebAssembly bootstrap script to throw an error when it's run in situations with many environment variables. One such situation is when the bootstrap script is run inside GitHub Actions. This change was introduced because the bootstrap script writes a copy of the environment variables into WebAssembly memory without any bounds checking, and writing more than 4096 bytes of data ends up writing past the end of the buffer and overwriting who-knows-what. So throwing an error in this situation is an improvement. However, this breaks esbuild which previously (at least seemingly) worked fine.
With this release, esbuild's WebAssembly bootstrap script that calls out to Go's WebAssembly bootstrap script will now delete all environment variables except for the ones that esbuild checks for, of which there are currently only four:
NO_COLOR,NODE_PATH,npm_config_user_agent, andWT_SESSION. This should avoid a crash when esbuild is built using Go 1.17.2+ and should reduce the likelihood of memory corruption when esbuild is built using Go 1.17.1 or earlier. This release also updates the Go version that esbuild ships with to version 1.17.2. Note that this problem only affects theesbuild-wasmpackage. Theesbuildpackage is not affected.See also:
-
Emit decorators for
declareclass fields (#1675)In version 3.7, TypeScript introduced the
declarekeyword for class fields that avoids generating any code for that field:// TypeScript input class Foo { a: number declare b: number } // JavaScript output class Foo { a; }However, it turns out that TypeScript still emits decorators for these omitted fields. With this release, esbuild will now do this too:
// TypeScript input class Foo { @decorator a: number; @decorator declare b: number; } // Old JavaScript output class Foo { a; } __decorateClass([ decorator ], Foo.prototype, "a", 2); // New JavaScript output class Foo { a; } __decorateClass([ decorator ], Foo.prototype, "a", 2); __decorateClass([ decorator ], Foo.prototype, "b", 2); -
Experimental support for esbuild on NetBSD (#1624)
With this release, esbuild now has a published binary executable for NetBSD in the
esbuild-netbsd-64npm package, and esbuild's installer has been modified to attempt to use it when on NetBSD. Hopefully this makes installing esbuild via npm work on NetBSD. This change was contributed by @gdt.⚠️ Note: NetBSD is not one of Node's supported platforms, so installing esbuild may or may not work on NetBSD depending on how Node has been patched. This is not a problem with esbuild. ⚠️
-
Disable the "esbuild was bundled" warning if
ESBUILD_BINARY_PATHis provided (#1678)The
ESBUILD_BINARY_PATHenvironment variable allows you to substitute an alternate binary executable for esbuild's JavaScript API. This is useful in certain cases such as when debugging esbuild. The JavaScript API has some code that throws an error if it detects that it was bundled before being run, since bundling prevents esbuild from being able to find the path to its binary executable. However, that error is unnecessary ifESBUILD_BINARY_PATHis present because an alternate path has been provided. This release disables the warning whenESBUILD_BINARY_PATHis present so that esbuild can be used when bundled as long as you also manually specifyESBUILD_BINARY_PATH.This change was contributed by @heypiotr.
-
Remove unused
catchbindings when minifying (#1660)With this release, esbuild will now remove unused
catchbindings when minifying:// Original code try { throw 0; } catch (e) { } // Old output (with --minify) try{throw 0}catch(t){} // New output (with --minify) try{throw 0}catch{}This takes advantage of the new optional catch binding syntax feature that was introduced in ES2019. This minification rule is only enabled when optional catch bindings are supported by the target environment. Specifically, it's not enabled when using
--target=es2018or older. Make sure to set esbuild'stargetsetting correctly when minifying if the code will be running in an older JavaScript environment.This change was contributed by @sapphi-red.
-
Improve watch mode accuracy (#1113)
Watch mode is enabled by
--watchand causes esbuild to become a long-running process that automatically rebuilds output files when input files are changed. It's implemented by recording all calls to esbuild's internal file system interface and then invalidating the build whenever these calls would return different values. For example, a call to esbuild's internalReadFile()function is considered to be different if either the presence of the file has changed (e.g. the file didn't exist before but now exists) or the presence of the file stayed the same but the content of the file has changed.Previously esbuild's watch mode operated at the
ReadFile()andReadDirectory()level. When esbuild checked whether a directory entry existed or not (e.g. whether a directory contains anode_modulessubdirectory or apackage.jsonfile), it calledReadDirectory()which then caused the build to depend on that directory's set of entries. This meant the build would be invalidated even if a new unrelated entry was added or removed, since that still changes the set of entries. This is problematic when using esbuild in environments that constantly create and destroy temporary directory entries in your project directory. In that case, esbuild's watch mode would constantly rebuild as the directory was constantly considered to be dirty.With this release, watch mode now operates at the
ReadFile()andReadDirectory().Get()level. So when esbuild checks whether a directory entry exists or not, the build should now only depend on the presence status for that one directory entry. This should avoid unnecessary rebuilds due to unrelated directory entries being added or removed. The log messages generated using--watchwill now also mention the specific directory entry whose presence status was changed if a build is invalidated for this reason.Note that this optimization does not apply to plugins using the
watchDirsreturn value because those paths are only specified at the directory level and do not describe individual directory entries. You can usewatchFilesorwatchDirson the individual entries inside the directory to get a similar effect instead. -
Disallow certain uses of
<in.mtsand.ctsfilesThe upcoming version 4.5 of TypeScript is introducing the
.mtsand.ctsextensions that turn into the.mjsand.cjsextensions when compiled. However, unlike the existing.tsand.tsxextensions, expressions that start with<are disallowed when they would be ambiguous depending on whether they are parsed in.tsor.tsxmode. The ambiguity is caused by the overlap between the syntax for JSX elements and the old deprecated syntax for type casts:Syntax .ts.tsx.mts/.cts<x>y✅ Type cast 🚫 Syntax error 🚫 Syntax error <T>() => {}✅ Arrow function 🚫 Syntax error 🚫 Syntax error <x>y</x>🚫 Syntax error ✅ JSX element 🚫 Syntax error <T>() => {}</T>🚫 Syntax error ✅ JSX element 🚫 Syntax error <T extends>() => {}</T>🚫 Syntax error ✅ JSX element 🚫 Syntax error <T extends={0}>() => {}</T>🚫 Syntax error ✅ JSX element 🚫 Syntax error <T,>() => {}✅ Arrow function ✅ Arrow function ✅ Arrow function <T extends X>() => {}✅ Arrow function ✅ Arrow function ✅ Arrow function This release of esbuild introduces a syntax error for these ambiguous syntax constructs in
.mtsand.ctsfiles to match the new behavior of the TypeScript compiler. -
Do not remove empty
@keyframesrules (#1665)CSS minification in esbuild automatically removes empty CSS rules, since they have no effect. However, empty
@keyframesrules still trigger JavaScript animation events so it's incorrect to remove them. To demonstrate that empty@keyframesrules still have an effect, here is a bug report for Firefox where it was incorrectly not triggering JavaScript animation events for empty@keyframesrules: https://bugzilla.mozilla.org/show_bug.cgi?id=1004377.With this release, empty
@keyframesrules are now preserved during minification:/* Original CSS */ @keyframes foo { from {} to {} } /* Old output (with --minify) */ /* New output (with --minify) */ @keyframes foo{}This fix was contributed by @eelco.
-
Fix an incorrect duplicate label error (#1671)
When labeling a statement in JavaScript, the label must be unique within the enclosing statements since the label determines the jump target of any labeled
breakorcontinuestatement:// This code is valid x: y: z: break x; // This code is invalid x: y: x: break x;However, an enclosing label with the same name is allowed as long as it's located in a different function body. Since
breakandcontinuestatements can't jump across function boundaries, the label is not ambiguous. This release fixes a bug where esbuild incorrectly treated this valid code as a syntax error:// This code is valid, but was incorrectly considered a syntax error x: (() => { x: break x; })();This fix was contributed by @nevkontakte.
-
Fix permission issues with the install script (#1642)
The
esbuildpackage contains a small JavaScript stub file that implements the CLI (command-line interface). Its only purpose is to spawn the binary esbuild executable as a child process and forward the command-line arguments to it.The install script contains an optimization that replaces this small JavaScript stub with the actual binary executable at install time to avoid the overhead of unnecessarily creating a new
nodeprocess. This optimization can't be done at package publish time because there is only oneesbuildpackage but there are many supported platforms, so the binary executable for the current platform must live outside of theesbuildpackage.However, the optimization was implemented with an unlink operation followed by a link operation. This means that if the first step fails, the package is left in a broken state since the JavaScript stub file is deleted but not yet replaced.
With this release, the optimization is now implemented with a link operation followed by a rename operation. This should always leave the package in a working state even if either step fails.
-
Add a fallback for
npm install esbuild --no-optional(#1647)The installation method for esbuild's platform-specific binary executable was recently changed in version 0.13.0. Before that version esbuild downloaded it in an install script, and after that version esbuild lets the package manager download it using the
optionalDependenciesfeature inpackage.json. This change was made because downloading the binary executable in an install script never really fully worked. The reasons are complex but basically there are a variety of edge cases where people people want to install esbuild in environments that they have customized such that downloading esbuild isn't possible. UsingoptionalDependenciesinstead lets the package manager deal with it instead, which should work fine in all cases (either that or your package manager has a bug, but that's not esbuild's problem).There is one case where this new installation method doesn't work: if you pass the
--no-optionalflag to npm to disable theoptionalDependenciesfeature. If you do this, you prevent esbuild from being installed. This is not a problem with esbuild because you are manually enabling a flag to change npm's behavior such that esbuild doesn't install correctly. However, people still want to do this.With this release, esbuild will now fall back to the old installation method if the new installation method fails. THIS MAY NOT WORK. The new
optionalDependenciesinstallation method is the only supported way to install esbuild with npm. The old downloading installation method was removed because it doesn't always work. The downloading method is only being provided to try to be helpful but it's not the supported installation method. If you pass--no-optionaland the download fails due to some environment customization you did, the recommended fix is to just remove the--no-optionalflag. -
Support the new
.mtsand.ctsTypeScript file extensionsThe upcoming version 4.5 of TypeScript has two new file extensions:
.mtsand.cts. Files with these extensions can be imported using the.mjsand.cjs, respectively. So the statementimport "./foo.mjs"in TypeScript can actually succeed even if the file./foo.mjsdoesn't exist on the file system as long as the file./foo.mtsdoes exist. The import path with the.mjsextension is automatically re-routed to the corresponding file with the.mtsextension at type-checking time by the TypeScript compiler. See the TypeScript 4.5 beta announcement for details.With this release, esbuild will also automatically rewrite
.mjsto.mtsand.cjsto.ctswhen resolving import paths to files on the file system. This should make it possible to bundle code written in this new style. In addition, the extensions.mtsand.ctsare now also considered valid TypeScript file extensions by default along with the.tsextension. -
Fix invalid CSS minification of
marginandpadding(#1657)CSS minification does collapsing of
marginandpaddingrelated properties. For example:/* Original CSS */ div { margin: auto; margin-top: 5px; margin-left: 5px; } /* Minified CSS */ div{margin:5px auto auto 5px}However, while this works for the
autokeyword, it doesn't work for other keywords. For example:/* Original CSS */ div { margin: inherit; margin-top: 5px; margin-left: 5px; } /* Minified CSS */ div{margin:inherit;margin-top:5px;margin-left:5px}Transforming this to
div{margin:5px inherit inherit 5px}, as was done in previous releases of esbuild, is an invalid transformation and results in incorrect CSS. This release of esbuild fixes this CSS transformation bug.
-
Support TypeScript type-only import/export specifiers (#1637)
This release adds support for a new TypeScript syntax feature in the upcoming version 4.5 of TypeScript. This feature lets you prefix individual imports and exports with the
typekeyword to indicate that they are types instead of values. This helps tools such as esbuild omit them from your source code, and is necessary because esbuild compiles files one-at-a-time and doesn't know at parse time which imports/exports are types and which are values. The new syntax looks like this:// Input TypeScript code import { type Foo } from 'foo' export { type Bar } // Output JavaScript code (requires "importsNotUsedAsValues": "preserve" in "tsconfig.json") import {} from "foo"; export {};See microsoft/TypeScript#45998 for full details. From what I understand this is a purely ergonomic improvement since this was already previously possible using a type-only import/export statements like this:
// Input TypeScript code import type { Foo } from 'foo' export type { Bar } import 'foo' export {} // Output JavaScript code (requires "importsNotUsedAsValues": "preserve" in "tsconfig.json") import "foo"; export {};This feature was contributed by @g-plane.
-
Fix
export {}statements with--tree-shaking=true(#1628)The new
--tree-shaking=trueoption allows you to force-enable tree shaking in cases where it wasn't previously possible. One such case is when bundling is disabled and there is no output format configured, in which case esbuild just preserves the format of whatever format the input code is in. Enabling tree shaking in this context caused a bug whereexport {}statements were stripped. This release fixes the bug soexport {}statements should now be preserved when you pass--tree-shaking=true. This bug only affected this new functionality and didn't affect existing scenarios.
-
Fix the
esbuildpackage in yarn 2+The yarn package manager version 2 and above has a mode called PnP that installs packages inside zip files instead of using individual files on disk, and then hijacks node's
fsmodule to pretend that paths to files inside the zip file are actually individual files on disk so that code that wasn't written specifically for yarn still works. Unfortunately that hijacking is incomplete and it still causes certain things to break such as using these zip file paths to create a JavaScript worker thread or to create a child process.This was an issue for the new
optionalDependenciespackage installation strategy that was just released in version 0.13.0 since the binary executable is now inside of an installed package instead of being downloaded using an install script. When it's installed with yarn 2+ in PnP mode the binary executable is inside a zip file and can't be run. To work around this, esbuild detects yarn's PnP mode and copies the binary executable to a real file outside of the zip file.Unfortunately the code to do this didn't create the parent directory before writing to the file path. That caused esbuild's API to crash when it was run for the first time. This didn't come up during testing because the parent directory already existed when the tests were run. This release changes the location of the binary executable from a shared cache directory to inside the esbuild package itself, which should fix this crash. This problem only affected esbuild's JS API when it was run through yarn 2+ with PnP mode active.
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.12.0. See the documentation about semver for more information.
-
Allow tree shaking to be force-enabled and force-disabled (#1518, #1610, #1611, #1617)
This release introduces a breaking change that gives you more control over when tree shaking happens ("tree shaking" here refers to declaration-level dead code removal). Previously esbuild's tree shaking was automatically enabled or disabled for you depending on the situation and there was no manual override to change this. Specifically, tree shaking was only enabled either when bundling was enabled or when the output format was set to
iife(i.e. wrapped in an immediately-invoked function expression). This was done to avoid issues with people appending code to output files in thecjsandesmformats and expecting that code to be able to reference code in the output file that isn't otherwise referenced.You now have the ability to explicitly force-enable or force-disable tree shaking to bypass this default behavior. This is a breaking change because there is already a setting for tree shaking that does something else, and it has been moved to a separate setting instead. The previous setting allowed you to control whether or not to ignore manual side-effect annotations, which is related to tree shaking since only side-effect free code can be removed as dead code. Specifically you can annotate function calls with
/* @__PURE__ */to indicate that they can be removed if they are not used, and you can annotate packages with"sideEffects": falseto indicate that imports of that package can be removed if they are not used. Being able to ignore these annotations is necessary because they are sometimes incorrect. This previous setting has been moved to a separate setting because it actually impacts dead-code removal within expressions, which also applies when minifying with tree-shaking disabled.Old behavior
- CLI
- Ignore side-effect annotations:
--tree-shaking=ignore-annotations
- Ignore side-effect annotations:
- JS
- Ignore side-effect annotations:
treeShaking: 'ignore-annotations'
- Ignore side-effect annotations:
- Go
- Ignore side-effect annotations:
TreeShaking: api.TreeShakingIgnoreAnnotations
- Ignore side-effect annotations:
New behavior
- CLI
- Ignore side-effect annotations:
--ignore-annotations - Force-disable tree shaking:
--tree-shaking=false - Force-enable tree shaking:
--tree-shaking=true
- Ignore side-effect annotations:
- JS
- Ignore side-effect annotations:
ignoreAnnotations: true - Force-disable tree shaking:
treeShaking: false - Force-enable tree shaking:
treeShaking: true
- Ignore side-effect annotations:
- Go
- Ignore side-effect annotations:
IgnoreAnnotations: true - Force-disable tree shaking:
TreeShaking: api.TreeShakingFalse - Force-enable tree shaking:
TreeShaking: api.TreeShakingTrue
- Ignore side-effect annotations:
- CLI
-
The npm package now uses
optionalDependenciesto install the platform-specific binary executable (#286, #291, #319, #347, #369, #547, #565, #789, #921, #1193, #1270, #1382, #1422, #1450, #1485, #1546, #1547, #1574, #1609)This release changes esbuild's installation strategy in an attempt to improve compatibility with edge cases such as custom registries, custom proxies, offline installations, read-only file systems, or when post-install scripts are disabled. It's being treated as a breaking change out of caution because it's a significant change to how esbuild works with JS package managers, and hasn't been widely tested yet.
The old installation strategy manually downloaded the correct binary executable in a post-install script. The binary executable is hosted in a separate platform-specific npm package such as
esbuild-darwin-64. The install script first attempted to download the package via thenpmcommand in case npm had custom network settings configured. If that didn't work, the install script attempted to download the package from https://registry.npmjs.org/ before giving up. This was problematic for many reasons including:- Not all of npm's settings can be forwarded due to npm bugs such as https://github.com/npm/cli/issues/2284, and npm has said these bugs will never be fixed.
- Some people have configured their network environments such that downloading from https://registry.npmjs.org/ will hang instead of either succeeding or failing.
- The installed package was broken if you used
npm --ignore-scriptsbecause then the post-install script wasn't run. Some people enable this option so that malicious packages must be run first before being able to do malicious stuff.
The new installation strategy automatically downloads the correct binary executable using npm's
optionalDependenciesfeature to depend on all esbuild packages for all platforms but only have the one for the current platform be installed. This is a built-in part of the package manager so my assumption is that it should work correctly in all of these edge cases that currently don't work. And if there's an issue with this, then the problem is with the package manager instead of with esbuild so this should hopefully reduce the maintenance burden on esbuild itself. Changing to this installation strategy has these drawbacks:-
Old versions of certain package managers (specifically npm and yarn) print lots of useless log messages during the installation, at least one for each platform other than the current one. These messages are harmless and can be ignored. However, they are annoying. There is nothing I can do about this. If you have this problem, one solution is to upgrade your package manager to a newer version.
-
Installation will be significantly slower in old versions of npm, old versions of pnpm, and all versions of yarn. These package managers download all packages for all platforms even though they aren't needed and actually cannot be used. This problem has been fixed in npm and pnpm and the problem has been communicated to yarn: https://github.com/yarnpkg/berry/issues/3317. If you have this problem, one solution is to use a newer version of npm or pnpm as your package manager.
-
This installation strategy does not work if you use
npm --no-optionalsince then the package with the binary executable is not installed. If you have this problem, the solution is to not pass the--no-optionalflag when installing packages. -
There is still a small post-install script but it's now optional in that the
esbuildpackage should still function correctly if post-install scripts are disabled (such as withnpm --ignore-scripts). This post-install script optimizes the installed package by replacing theesbuildJavaScript command shim with the actual binary executable at install time. This avoids the overhead of launching anothernodeprocess when using theesbuildcommand. So keep in mind that installing with--ignore-scriptswill result in a sloweresbuildcommand.
Despite the drawbacks of the new installation strategy, I believe this change is overall a good thing to move forward with. It should fix edge case scenarios where installing esbuild currently doesn't work at all, and this only comes at the expense of the install script working in a less-optimal way (but still working) if you are using an old version of npm. So I'm going to switch installation strategies and see how it goes.
The platform-specific binary executables are still hosted on npm in the same way, so anyone who wrote code that downloads builds from npm using the instructions here should not have to change their code: https://esbuild.github.io/getting-started/#download-a-build. However, note that these platform-specific packages no longer specify the
binfield inpackage.jsonso theesbuildcommand will no longer be automatically put on your path. Thebinfield had to be removed because of a collision with thebinfield of theesbuildpackage (now that theesbuildpackage depends on all of these platform-specific packages as optional dependencies).
In addition to the breaking changes above, the following features are also included in this release:
-
Treat
xguarded bytypeof x !== 'undefined'as side-effect freeThis is a small tree-shaking (i.e. dead code removal) improvement. Global identifier references are considered to potentially have side effects since they will throw a reference error if the global identifier isn't defined, and code with side effects cannot be removed as dead code. However, there's a somewhat-common case where the identifier reference is guarded by a
typeofcheck to check that it's defined before accessing it. With this release, code that does this will now be considered to have no side effects which allows it to be tree-shaken:// Original code var __foo = typeof foo !== 'undefined' && foo; var __bar = typeof bar !== 'undefined' && bar; console.log(__bar); // Old output (with --bundle, which enables tree-shaking) var __foo = typeof foo !== 'undefined' && foo; var __bar = typeof bar !== 'undefined' && bar; console.log(__bar); // New output (with --bundle, which enables tree-shaking) var __bar = typeof bar !== 'undefined' && bar; console.log(__bar);
-
Fix compilation of abstract class fields in TypeScript (#1623)
This release fixes a bug where esbuild could incorrectly include a TypeScript abstract class field in the compiled JavaScript output. This is incorrect because the official TypeScript compiler never does this. Note that this only happened in scenarios where TypeScript's
useDefineForClassFieldssetting was set totrue(or equivalently where TypeScript'stargetsetting was set toESNext). Here is the difference:// Original code abstract class Foo { abstract foo: any; } // Old output class Foo { foo; } // New output class Foo { } -
Proxy from the
__requireshim torequire(#1614)Some background: esbuild's bundler emulates a CommonJS environment. The bundling process replaces the literal syntax
require(<string>)with the referenced module at compile-time. However, other uses ofrequiresuch asrequire(someFunction())are not bundled since the value ofsomeFunction()depends on code evaluation, and esbuild does not evaluate code at compile-time. So it's possible for some references torequireto remain after bundling.This was causing problems for some CommonJS code that was run in the browser and that expected
typeof require === 'function'to be true (see #1202), since the browser does not provide a global calledrequire. Thus esbuild introduced a shimrequirefunction called__require(shown below) and replaced all references torequirein the bundled code with__require:var __require = x => { if (typeof require !== 'undefined') return require(x); throw new Error('Dynamic require of "' + x + '" is not supported'); };However, this broke code that referenced
require.resolveinside the bundle, which could hypothetically actually work since you could assign your own implementation towindow.require.resolve(see #1579). So the implementation of__requirewas changed to this:var __require = typeof require !== 'undefined' ? require : x => { throw new Error('Dynamic require of "' + x + '" is not supported'); };However, that broke code that assigned to
window.requirelater on after the bundle was loaded (#1614). So with this release, the code for__requirenow handles all of these edge cases:typeof requireis stillfunctioneven ifwindow.requireis undefinedwindow.requirecan be assigned to either before or after the bundle is loadedrequire.resolveand arbitrary other properties can still be accessedrequirewill now forward any number of arguments, not just the first one
Handling all of these edge cases is only possible with the Proxy API. So the implementation of
__requirenow looks like this:var __require = (x => typeof require !== 'undefined' ? require : typeof Proxy !== 'undefined' ? new Proxy(x, { get: (a, b) => (typeof require !== 'undefined' ? require : a)[b] }) : x )(function(x) { if (typeof require !== 'undefined') return require.apply(this, arguments); throw new Error('Dynamic require of "' + x + '" is not supported'); }); -
Consider
typeof xto have no side effectsThe
typeofoperator does not itself trigger any code evaluation so it can safely be removed if evaluating the operand does not cause any side effects. However, there is a special case of thetypeofoperator when the operand is an identifier expression. In that case no reference error is thrown if the referenced symbol does not exist (e.g.typeof xdoes not throw an error if there is no symbol namedx). With this release, esbuild will now considertypeof xto have no side effects even if evaluatingxwould have side effects (i.e. would throw a reference error):// Original code var unused = typeof React !== 'undefined'; // Old output var unused = typeof React !== 'undefined'; // New outputNote that there is actually an edge case where
typeof xcan throw an error: whenxis being referenced inside of its TDZ, or temporal dead zone (i.e. before it's declared). This applies tolet,const, andclasssymbols. However, esbuild doesn't currently handle TDZ rules so the possibility of errors thrown due to TDZ rules is not currently considered. This typically doesn't matter in real-world code so this hasn't been a priority to fix (and is actually tricky to fix with esbuild's current bundling approach). So esbuild may incorrectly remove atypeofexpression that actually has side effects. However, esbuild already incorrectly did this in previous releases so its behavior regardingtypeofand TDZ rules hasn't changed in this release.
-
Fix U+30FB and U+FF65 in identifier names in ES5 vs. ES6+ (#1599)
The ES6 specification caused two code points that were previously valid in identifier names in ES5 to no longer be valid in identifier names in ES6+. The two code points are:
U+30FBi.e.KATAKANA MIDDLE DOTi.e.・U+FF65i.e.HALFWIDTH KATAKANA MIDDLE DOTi.e.・
This means that using ES6+ parsing rules will fail to parse some valid ES5 code, and generating valid ES5 code may fail to be parsed using ES6+ parsing rules. For example, esbuild would previously fail to parse
x.y・even though it's valid ES5 code (since it's not valid ES6+ code) and esbuild could generate{y・:x}when minifying even though it's not valid ES6+ code (since it's valid ES5 code). This problem is the result of my incorrect assumption that ES6 is a superset of ES5.As of this release, esbuild will now parse a superset of ES5 and ES6+ and will now quote identifier names when possible if it's not considered to be a valid identifier name in either ES5 or ES6+. In other words, a union of ES5 and ES6 rules is used for parsing and the intersection of ES5 and ES6 rules is used for printing.
-
Fix
++and--on class private fields when used with big integers (#1600)Previously when esbuild lowered class private fields (e.g.
#foo) to older JavaScript syntax, the transform of the++and--was not correct if the value is a big integer such as123n. The transform in esbuild is similar to Babel's transform which has the same problem. Specifically, the code was transformed into code that either adds or subtracts the number1and123n + 1throws an exception in JavaScript. This problem has been fixed so this should now work fine starting with this release.
-
Update JavaScript syntax feature compatibility tables (#1594)
Most JavaScript syntax feature compatibility data is able to be obtained automatically via https://kangax.github.io/compat-table/. However, they are missing data for quite a few new JavaScript features (see (kangax/compat-table#1034)) so data on these new features has to be added manually. This release manually adds a few new entries:
-
Top-level await
This feature lets you use
awaitat the top level of a module, outside of anasyncfunction. Doing this holds up the entire module instantiation operation until the awaited expression is resolved or rejected. This release marks this feature as supported in Edge 89, Firefox 89, and Safari 15 (it was already marked as supported in Chrome 89 and Node 14.8). The data source for this is https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await. -
Arbitrary module namespace identifier names
This lets you use arbitrary strings as module namespace identifier names as long as they are valid UTF-16 strings. An example is
export { x as "🍕" }which can then be imported asimport { "🍕" as y } from "./example.js". This release marks this feature as supported in Firefox 87 (it was already marked as supported in Chrome 90 and Node 16). The data source for this is https://bugzilla.mozilla.org/show_bug.cgi?id=1670044.
I would also like to add data for Safari. They have recently added support for arbitrary module namespace identifier names (https://bugs.webkit.org/show_bug.cgi?id=217576) and
export * as(https://bugs.webkit.org/show_bug.cgi?id=214379). However, I have no idea how to determine which Safari release these bugs correspond to so this compatibility data for Safari has been omitted. -
-
Avoid unnecessary additional log messages after the server is stopped (#1589)
There is a development server built in to esbuild which is accessible via the
serve()API call. This returns a promise that resolves to an object with astop()method that immediately terminates the development server. Previously calling this could cause esbuild to print stray log messages sincestop()could cause plugins to be unregistered while a build is still in progress. With this release, callingstop()no longer terminates the development server immediately. It now waits for any active builds to finish first so the builds are not interrupted and left in a confusing state. -
Fix an accidental dependency on Go ≥1.17.0 (#1585)
The source code of this release no longer uses the
math.MaxIntconstant that was introduced in Go version 1.17.0. This constant was preventing esbuild from being compiled on Go version <1.17.0. This fix was contributed by @davezuko.
-
Add
--analyzeto print information about the bundle (#1568)The
--metafile=flag tells esbuild to write information about the bundle into the provided metadata file in JSON format. It contains information about the input files and which other files each one imports, as well as the output files and which input files they include. This information is sufficient to answer many questions such as:- Which files are in my bundle?
- What's are the biggest files in my bundle?
- Why is this file included in my bundle?
Previously you had to either write your own code to answer these questions, or use another tool such as https://bundle-buddy.com/esbuild to visualize the data. Starting with this release you can now also use
--analyzeto enable esbuild's built-in visualizer. It looks like this:$ esbuild --bundle example.jsx --outfile=out.js --minify --analyze out.js 27.6kb ⚡ Done in 6ms out.js 27.6kb 100.0% ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js 19.2kb 69.8% ├ node_modules/react/cjs/react.production.min.js 5.9kb 21.4% ├ node_modules/object-assign/index.js 965b 3.4% ├ example.jsx 137b 0.5% ├ node_modules/react-dom/server.browser.js 50b 0.2% └ node_modules/react/index.js 50b 0.2%This tells you what input files were bundled into each output file as well as the final minified size contribution of each input file as well as the percentage of the output file it takes up. You can also enable verbose analysis with
--analyze=verboseto see why each input file was included (i.e. which files imported it from the entry point file):$ esbuild --bundle example.jsx --outfile=out.js --minify --analyze=verbose out.js 27.6kb ⚡ Done in 6ms out.js ─────────────────────────────────────────────────────────────────── 27.6kb ─ 100.0% ├ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js ─ 19.2kb ── 69.8% │ └ node_modules/react-dom/server.browser.js │ └ example.jsx ├ node_modules/react/cjs/react.production.min.js ───────────────────────── 5.9kb ── 21.4% │ └ node_modules/react/index.js │ └ example.jsx ├ node_modules/object-assign/index.js ──────────────────────────────────── 965b ──── 3.4% │ └ node_modules/react-dom/cjs/react-dom-server.browser.production.min.js │ └ node_modules/react-dom/server.browser.js │ └ example.jsx ├ example.jsx ──────────────────────────────────────────────────────────── 137b ──── 0.5% ├ node_modules/react-dom/server.browser.js ──────────────────────────────── 50b ──── 0.2% │ └ example.jsx └ node_modules/react/index.js ───────────────────────────────────────────── 50b ──── 0.2% └ example.jsxThere is also a JS API for this:
const result = await esbuild.build({ metafile: true, ... }) console.log(await esbuild.analyzeMetafile(result.metafile, { verbose: true, }))and a Go API:
result := api.Build(api.BuildOptions{ Metafile: true, ... }) fmt.Println(api.AnalyzeMetafile(result.Metafile, api.AnalyzeMetafileOptions{ Verbose: true, }))Note that this is not the only way to visualize this data. If you want a visualization that's different than the information displayed here, you can easily build it yourself using the information in the metafile that is generated with the
--metafile=flag.Also note that this data is intended for humans, not machines. The specific format of this data may change over time which will likely break any tools that try to parse it. You should not write a tool to parse this data. You should be using the information in the JSON metadata file instead. Everything in this visualization is derived from the JSON metadata so you are not losing out on any information by not using esbuild's output.
-
Allow
require.resolvein non-node builds (#1579)With this release, you can now use
require.resolvein builds when the target platform is set tobrowserinstead ofnodeas long as the functionwindow.require.resolveexists somehow. This was already possible when the platform isnodebut when the platform isbrowser, esbuild generates a no-op shimrequirefunction for compatibility reasons (e.g. because some code expectstypeof requiremust be"function"even in the browser). The shim previously had a fallback towindow.requireif it exists, but additional properties of therequirefunction such asrequire.resolvewere not copied over to the shim. Now the shim function is only used ifwindow.requireis undefined so additional properties such asrequire.resolveshould now work.This change was contributed by @screetBloom.
-
Fix a TypeScript parsing edge case with the postfix
!operator (#1560)This release fixes a bug with esbuild's TypeScript parser where the postfix
!operator incorrectly terminated a member expression after thenewoperator:// Original input new Foo!.Bar(); // Old output new Foo().Bar(); // New output new Foo.Bar();The problem was that
!was considered a postfix operator instead of part of a member expression. It is now considered to be part of a member expression instead, which fixes this edge case. -
Fix a parsing crash with nested private brand checks
This release fixes a bug in the parser where code of the form
#a in #b in ccaused a crash. This code now causes a syntax error instead. Private identifiers are allowed when followed byin, but only if the operator precedence level is such that theinoperator is allowed. The parser was missing the operator precedence check. -
Publish x86-64 binary executables for illumos (#1562)
This release adds support for the illumos operating system, which is related to Solaris and SunOS. Support for this platform was contributed by @hadfl.
-
Fix an edge case with direct
evaland variable renamingUse of the direct
evalconstruct causes all variable names in the scope containing the directevaland all of its parent scopes to become "pinned" and unable to be renamed. This is because the dynamically-evaluated code is allowed to reference any of those variables by name. When this happens esbuild avoids renaming any of these variables, which effectively disables minification for most of the file, and avoids renaming any non-pinned variables to the name of a pinned variable.However, there was previously a bug where the pinned variable name avoidance only worked for pinned variables in the top-level scope but not in nested scopes. This could result in a non-pinned variable being incorrectly renamed to the name of a pinned variable in certain cases. For example:
// Input to esbuild return function($) { function foo(arg) { return arg + $; } // Direct "eval" here prevents "$" from being renamed // Repeated "$" puts "$" at the top of the character frequency histogram return eval(foo($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$)) }(2);When this code is minified with
--minify-identifiers, the non-pinned variableargis incorrectly transformed into$resulting in a name collision with the nested pinned variable$:// Old output from esbuild (incorrect) return function($) { function foo($) { return $ + $; } return eval(foo($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$)); }(2);This is because the non-pinned variable
argis renamed to the top character in the character frequency histogram$(esbuild uses a character frequency histogram for smaller gzipped output sizes) and the pinned variable$was incorrectly not present in the list of variable names to avoid. With this release, the output is now correct:// New output from esbuild (correct) return function($) { function foo(n) { return n + $; } return eval(foo($$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$)); }(2);Note that even when esbuild handles direct
evalcorrectly, using directevalis not recommended because it disables minification for the file and likely won't work correctly in the presence of scope hoisting optimizations. See https://esbuild.github.io/link/direct-eval for more details.
-
Parsing of rest arguments in certain TypeScript types (#1553)
This release implements parsing of rest arguments inside object destructuring inside arrow functions inside TypeScript type declarations. Support for rest arguments in this specific syntax was not previously implemented. The following code was incorrectly considered a syntax error before this release, but is no longer considered a syntax error:
type F = ({ ...rest }) => void; -
Fix error message for
watch: trueandbuildSync(#1552)Watch mode currently only works with the
buildAPI. Previously using watch mode with thebuildSyncAPI caused a confusing error message. This release explicitly disallows doing this, so the error message is now more clear. -
Fix an minification bug with the
--keep-namesoption (#1552)This release fixes a subtle bug that happens with
--keep-names --minifyand nested function declarations in strict mode code. It can be triggered by the following code, which was being compiled incorrectly under those flags:export function outer() { { function inner() { return Math.random(); } const x = inner(); console.log(x); } } outer();The bug was caused by an unfortunate interaction between a few of esbuild's behaviors:
-
Function declarations inside of nested scopes behave differently in different situations, so esbuild rewrites this function declaration to a local variable initialized to a function expression instead so that it behaves the same in all situations.
More specifically, the interpretation of such function declarations depends on whether or not it currently exists in a strict mode context:
> (function(){ { function x(){} } return x })() function x() {} > (function(){ 'use strict'; { function x(){} } return x })() ❌ Uncaught ReferenceError: x is not definedThe bundling process sometimes erases strict mode context. For example, different files may have different strict mode status but may be merged into a single file which all shares the same strict mode status. Also, files in ESM format are automatically in strict mode but a bundle output file in IIFE format may not be executed in strict mode. Transforming the nested
functionto aletin strict mode and avarin non-strict mode means esbuild's output will behave reliably in different environments. -
The "keep names" feature adds automatic calls to the built-in
__namehelper function to assign the original name to the.nameproperty of the minified function object at run-time. That transforms the code into this:let inner = function() { return Math.random(); }; __name(inner, "inner"); const x = inner(); console.log(x);This injected helper call does not count as a use of the associated function object so that dead-code elimination will still remove the function object as dead code if nothing else uses it. Otherwise dead-code elimination would stop working when the "keep names" feature is enabled.
-
Minification enables an optimization where an initialized variable with a single use immediately following that variable is transformed by inlining the initializer into the use. So for example
var a = 1; return ais transformed intoreturn 1. This code matches this pattern (initialized single-use variable + use immediately following that variable) so the optimization does the inlining, which transforms the code into this:__name(function() { return Math.random(); }, "inner"); const x = inner(); console.log(x);The code is now incorrect because
inneractually has two uses, although only one was actually counted.
This inlining optimization will now be avoided in this specific case, which fixes the bug without regressing dead-code elimination or initialized variable inlining in any other cases.
-
-
Make HTTP range requests more efficient (#1536)
The local HTTP server built in to esbuild supports range requests, which are necessary for video playback in Safari. This means you can now use
<video>tags in your HTML pages with esbuild's local HTTP server.Previously this was implemented inefficiently for files that aren't part of the build, but that are read from the underlying fallback directory. In that case the entire file was being read even though only part of the file was needed. In this release, only the part of the file that is needed is read so using HTTP range requests with esbuild in this case will now use less memory.
-
Fix CSS minification bug with
box-shadowandvar()(#1538)The
box-shadowproperty can be specified using 2, 3, or 4 numbers. The 3rd and 4th numbers are the blur radius and spread radius, and can be omitted if zero. When minifying, esbuild has an optimization that removes trailing zeros from runs of numbers within thebox-shadowproperty. However, that optimization is not correct in the presence of tokens that are neither a number, a color, nor the tokeninsert. These edge cases includevar()orcalc()tokens. With this release, esbuild will now do stronger validation and will only remove trailing zeros if the contents of thebox-shadowproperty matches the underlying CSS grammar exactly./* Original code */ button { box-shadow: 0 0 0 var(--spread) red; } /* Old minified output */ button{box-shadow:0 0 var(--spread) red} /* New minified output */ button{box-shadow:0 0 0 var(--spread) red}
-
Add support for native esbuild on Windows 64-bit ARM (#995)
The newly-released Go version 1.17.0 adds support for Windows 64-bit ARM CPUs, so esbuild can now support these CPUs as well. This release introduces support for
npm install esbuildon Windows 64-bit ARM.
-
Avoid the sequence
</stylein CSS output (#1509)The CSS code generator now avoids generating the character sequence
</stylein case you want to embed the CSS output in a<style>...</style>tag inside HTML:/* Original code */ a:after { content: "</style>"; } /* Old output */ a:after { content: "</style>"; } /* New output */ a:after { content: "<\/style>"; }This mirrors how the JS code generator similarly avoids the character sequence
</script.In addition, the check that escapes
</styleand</scriptis now case-insensitive to match how the browser's HTML parser behaves. So</STYLEand</SCRIPTare now escaped as well. -
Fix a TypeScript parsing edge case with ASI (Automatic Semicolon Insertion) (#1512)
This fixes a parsing bug where TypeScript types consisting of multiple identifiers joined together with a
.could incorrectly extend onto the next line if the next line started with<. This problem was due to ASI; esbuild should be automatically inserting a semicolon at the end of the line:let x: { <A extends B>(): c.d /* A semicolon should be automatically inserted here */ <E extends F>(): g.h }Previously the above code was incorrectly considered a syntax error since esbuild attempted to parse the parameterized type
c.d<E extends F ? ...>. With this release, this code is now parsed correctly.
-
Add support for CSS source maps (#519)
With this release, esbuild will now generate source maps for CSS output files when
--sourcemapis enabled. This supports all of the same options as JS source maps including--sourcemap=inlineand--sourcemap=external. In addition, CSS input files with embedded/*# sourceMappingURL=... */comments will cause the CSS output file source map to map all the way back to the original inputs. CSS source maps are used by the browser's style inspector to link back to the original source code instead of linking to the bundled source code. -
Fix computed class fields in TypeScript edge case (#1507)
If TypeScript code contains computed class fields, the target environment supports class fields so syntax lowering is not necessary, and TypeScript's
useDefineForClassFieldssetting is set totrue, then esbuild had a bug where the computed property names were computed after the class definition and were undefined. Note that TypeScript'suseDefineForClassFieldssetting defaults totrueiftsconfig.jsoncontains"target": "ESNext".// Original code class Foo { [foo] = 1; @bar [baz] = 2; } // Old output var _a, _b; var Foo = class { [_a] = 1; [_b] = 2; }; _a = foo, _b = baz; __decorateClass([ bar ], Foo.prototype, _b, 2); // New output var _a; var Foo = class { [foo] = 1; [_a = baz] = 2; }; __decorateClass([ bar ], Foo.prototype, _a, 2);The problem in this case is that normally TypeScript moves class field initializers into the special
constructormethod (automatically generating one if one doesn't already exist) so the side effects for class field property names must happen after the class body. But if class fields are supported by the target environment then the side effects must happen inline instead.
-
Allow implicit
./in CSS@importpaths (#1494)In the browser, the paths inside CSS
@importrules are implicitly relative to the path of the current CSS style sheet. Previously esbuild used node's JS path resolution rules in CSS as well, which required a./or../prefix for a path to be considered a relative path. Paths without that prefix are considered package paths and are searched for insidenode_modulesinstead.With this release, esbuild will now first try to interpret the path as a relative path and then fall back to interpreting it as a package path if nothing exists at that relative path. This feature was originally added in version 0.7.18 but only worked for CSS
url()tokens. In this release it now also works for@importrules.This feature was contributed by @pd4d10.
-
Fix lowering of nullish coalescing assignment edge case (#1493)
This release fixes a bug where lowering of the
??=nullish coalescing assignment operator failed when the target environment supported nullish coalescing and private class fields but not nullish coalescing assignment. An example target environment with this specific feature support matrix combination is node 14.8. This edge case is now lowered correctly:// Original code class A { #a; f() { this.#a ??= 1; } } // Old output (with --target=node14.8) panic: Unexpected expression of type *js_ast.EPrivateIdentifier // New output (with --target=node14.8) class A { #a; f() { this.#a ?? (this.#a = 1); } } -
Fix public fields being inserted before
super()call (#1497)The helper function that esbuild uses to emulate the new public class field syntax can potentially be inserted into the class constructor before the
super()call. That is problematic because the helper function makes use ofthis, andthismust only be used after thesuper()call. This release fixes a case where this happens when minification is enabled:// Original code class A extends B { x; constructor() { f(); super(); } } // Old output (with --minify-syntax --target=es6) class A extends B { constructor() { __publicField(this, "x"); f(), super(); } } // New output (with --minify-syntax --target=es6) class A extends B { constructor() { f(); super(); __publicField(this, "x"); } } -
Fix lowering of static private methods in class expressions (#1498)
Previously static private methods were lowered incorrectly when present in class expressions. The class expression itself was missing in the output due to an oversight (variable shadowing). This issue has been fixed:
// Original code (class { static #x() {} }); // Old output (with --target=es6) var _x, _a, x_fn; __privateAdd(_a, _x), _x = new WeakSet(), x_fn = function() { }, __privateAdd(_a, _x), _a; // New output (with --target=es6) var _x, _a, x_fn; _a = class { }, _x = new WeakSet(), x_fn = function() { }, __privateAdd(_a, _x), _a;
-
Fix a bug with private fields and logical assignment operators (#1418)
This release fixes a bug where code using private fields in combination with logical assignment operators was transformed incorrectly if the target environment supported logical assignment operators but not private fields. Since logical assignment operators are assignment operators, the entire operator must be transformed even if the operator is supported. This should now work correctly:
// Original code class Foo { #x foo() { this.#x &&= 2 this.#x ||= 2 this.#x ??= 2 } } // Old output var _x; class Foo { constructor() { __privateAdd(this, _x, void 0); } foo() { this._x &&= 2; this._x ||= 2; this._x ??= 2; } } _x = new WeakMap(); // New output var _x, _a; class Foo { constructor() { __privateAdd(this, _x, void 0); } foo() { __privateGet(this, _x) && __privateSet(this, _x, 2); __privateGet(this, _x) || __privateSet(this, _x, 2); __privateGet(this, _x) ?? __privateSet(this, _x, 2); } } _x = new WeakMap(); -
Fix a hoisting bug in the bundler (#1455)
This release fixes a bug where variables declared using
varinside of top-levelforloop initializers were not hoisted inside lazily-initialized ES modules (such as those that are generated when bundling code that loads an ES module usingrequire). This meant that hoisted function declarations incorrectly didn't have access to these loop variables:// entry.js console.log(require('./esm-file').test()) // esm-file.js for (var i = 0; i < 10; i++) ; export function test() { return i }Old output (incorrect):
// esm-file.js var esm_file_exports = {}; __export(esm_file_exports, { test: () => test }); function test() { return i; } var init_esm_file = __esm({ "esm-file.js"() { for (var i = 0; i < 10; i++) ; } }); // entry.js console.log((init_esm_file(), esm_file_exports).test());New output (correct):
// esm-file.js var esm_file_exports = {}; __export(esm_file_exports, { test: () => test }); function test() { return i; } var i; var init_esm_file = __esm({ "esm-file.js"() { for (i = 0; i < 10; i++) ; } }); // entry.js console.log((init_esm_file(), esm_file_exports).test()); -
Fix a code generation bug for private methods (#1424)
This release fixes a bug where when private methods are transformed and the target environment is one that supports private methods (such as
esnext), the member function name was uninitialized and took on the zero value by default. This resulted in the member function name becoming__createinstead of the correct name since that's the name of the symbol at index 0. Now esbuild always generates a private method symbol even when private methods are supported, so this is no longer an issue:// Original code class Foo { #a() { return 'a' } #b() { return 'b' } static c } // Old output var _a, __create, _b, __create; var Foo = class { constructor() { __privateAdd(this, _a); __privateAdd(this, _b); } }; _a = new WeakSet(); __create = function() { return "a"; }; _b = new WeakSet(); __create = function() { return "b"; }; __publicField(Foo, "c"); // New output var _a, a_fn, _b, b_fn; var Foo = class { constructor() { __privateAdd(this, _a); __privateAdd(this, _b); } }; _a = new WeakSet(); a_fn = function() { return "a"; }; _b = new WeakSet(); b_fn = function() { return "b"; }; __publicField(Foo, "c"); -
The CLI now stops watch and serve mode when stdin is closed (#1449)
To facilitate esbuild being called from the Erlang VM, esbuild's command-line interface will now exit when in
--watchor--servemode if stdin is closed. This change is necessary because the Erlang VM doesn't have an API for terminating a child process, so it instead closes stdin to indicate that the process is no longer needed.Note that this only happens when stdin is not a TTY (i.e. only when the CLI is being used non-interactively) to avoid disrupting the use case of manually moving esbuild to a background job using a Unix terminal.
This change was contributed by @josevalim.
-
Remove warning about bad CSS
@-rules (#1426)The CSS bundler built in to esbuild is only designed with real CSS in mind. Running other languages that compile down to CSS through esbuild without compiling them down to CSS first can be a bad idea since esbuild applies browser-style error recovery to invalid syntax and uses browser-style import order that other languages might not be expecting. This is why esbuild previously generated warnings when it encountered unknown CSS
@-rules.However, some people want to run other non-CSS languages through esbuild's CSS bundler anyway. So with this release, esbuild will no longer generate any warnings if you do this. But keep in mind that doing this is still potentially unsafe. Depending on the input language, using esbuild's CSS bundler to bundle non-CSS code can still potentially alter the semantics of your code.
-
Allow
ES2021intsconfig.json(#1470)TypeScript recently added support for
ES2021intsconfig.jsonso esbuild now supports this too. This has the same effect as if you passed--target=es2021to esbuild. Keep in mind that the value oftargetintsconfig.jsonis only respected if you did not pass a--target=value to esbuild. -
Avoid using the
worker_threadsoptimization in certain old node versions (#1462)The
worker_threadsoptimization makes esbuild's synchronous API calls go much faster than they would otherwise. However, it turns out this optimization cannot be used in certain node versions older thanv12.17.0, where node throws an error when trying to create the worker. This optimization is now disabled in these scenarios.Note that these old node versions are currently in maintenance. I recommend upgrading to a modern version of node if run-time performance is important to you.
-
Paths starting with
node:are implicitly external when bundling for node (#1466)This replicates a new node feature where you can prefix an import path with
node:to load a native node module by that name (such asimport fs from "node:fs/promises"). These paths also have special behavior:Core modules can also be identified using the
node:prefix, in which case it bypasses therequirecache. For instance,require('node:http')will always return the built in HTTP module, even if there isrequire.cacheentry by that name.With this release, esbuild's built-in resolver will now automatically consider all import paths starting with
node:as external. This new behavior is only active when the current platform is set to node such as with--platform=node. If you need to customize this behavior, you can write a plugin to intercept these paths and treat them differently. -
Consider
\and/to be the same in file paths (#1459)On Windows, there are many different file paths that can refer to the same underlying file. Windows uses a case-insensitive file system so for example
foo.jsandFoo.jsare the same file. When bundling, esbuild needs to treat both of these paths as the same to avoid incorrectly bundling the file twice. This is case is already handled by identifying files by their lower-case file path.The case that wasn't being handled is the fact that Windows supports two different path separators,
/and\, both of which mean the same thing. For examplefoo/bar.jsandfoo\bar.jsare the same file. With this release, this case is also handled by esbuild. Files that are imported in multiple places with inconsistent path separators will now be considered the same file instead of bundling the file multiple times.
-
Fix a bug with
var()in CSS color lowering (#1421)This release fixes a bug with esbuild's handling of the
rgbandhslcolor functions when they containvar(). Eachvar()token sequence can be substituted for any number of tokens including zero or more than one, but previously esbuild's output was only correct if eachvar()inside ofrgborhslcontained exactly one token. With this release, esbuild will now not attempt to transform newer CSS color syntax to older CSS color syntax if it containsvar():/* Original code */ a { color: hsl(var(--hs), var(--l)); } /* Old output */ a { color: hsl(var(--hs), ,, var(--l)); } /* New output */ a { color: hsl(var(--hs), var(--l)); }The bug with the old output above happened because esbuild considered the arguments to
hslas matching the patternhsl(h s l)which is the new space-separated form allowed by CSS Color Module Level 4. Then esbuild tried to convert this to the formhsl(h, s, l)which is more widely supported by older browsers. But this substitution doesn't work in the presence ofvar(), so it has now been disabled in that case.
-
Fix the
fileloader with custom namespaces (#1404)This fixes a regression from version 0.12.12 where using a plugin to load an input file with the
fileloader in a custom namespace caused esbuild to write the contents of that input file to the path associated with that namespace instead of to a path inside of the output directory. With this release, thefileloader should now always copy the file somewhere inside of the output directory.
-
Fix using JS synchronous API from from non-main threads (#1406)
This release fixes an issue with the new implementation of the synchronous JS API calls (
transformSyncandbuildSync) when they are used from a thread other than the main thread. The problem happened because esbuild's new implementation uses node'sworker_threadslibrary internally and non-main threads were incorrectly assumed to be esbuild's internal thread instead of potentially another unrelated thread. Now esbuild's synchronous JS APIs should work correctly when called from non-main threads.
-
Fix
fileloader import paths when subdirectories are present (#1044)Using the
fileloader for a file type causes importing affected files to copy the file into the output directory and to embed the path to the copied file into the code that imported it. However, esbuild previously always embedded the path relative to the output directory itself. This is problematic when the importing code is generated within a subdirectory inside the output directory, since then the relative path is wrong. For example:$ cat src/example/entry.css div { background: url(../images/image.png); } $ esbuild --bundle src/example/entry.css --outdir=out --outbase=src --loader:.png=file $ find out -type f out/example/entry.css out/image-55DNWN2R.png $ cat out/example/entry.css /* src/example/entry.css */ div { background: url(./image-55DNWN2R.png); }This is output from the previous version of esbuild. The above asset reference in
out/example/entry.cssis wrong. The path should start with../because the two files are in different directories.With this release, the asset references present in output files will now be the full relative path from the output file to the asset, so imports should now work correctly when the entry point is in a subdirectory within the output directory. This change affects asset reference paths in both CSS and JS output files.
Note that if you want asset reference paths to be independent of the subdirectory in which they reside, you can use the
--public-pathsetting to provide the common path that all asset reference paths should be constructed relative to. Specifically--public-path=.should bring back the old problematic behavior in case you need it. -
Add support for
[dir]in--asset-names(#1196)You can now use path templates such as
--asset-names=[dir]/[name]-[hash]to copy the input directory structure of your asset files (i.e. input files loaded with thefileloader) to the output directory. Here's an example:$ cat entry.css header { background: url(images/common/header.png); } main { background: url(images/home/hero.png); } $ esbuild --bundle entry.css --outdir=out --asset-names=[dir]/[name]-[hash] --loader:.png=file $ find out -type f out/images/home/hero-55DNWN2R.png out/images/common/header-55DNWN2R.png out/entry.css $ cat out/entry.css /* entry.css */ header { background: url(./images/common/header-55DNWN2R.png); } main { background: url(./images/home/hero-55DNWN2R.png); }
-
Enable faster synchronous transforms with the JS API by default (#1000)
Currently the synchronous JavaScript API calls
transformSyncandbuildSyncspawn a new child process on every call. This is due to limitations with node'schild_processAPI. Doing this meanstransformSyncandbuildSyncare much slower thantransformandbuild, which share the same child process across calls.This release improves the performance of
transformSyncandbuildSyncby up to 20x. It enables a hack where node'sworker_threadsAPI and atomics are used to block the main thread while asynchronous communication with a single long-lived child process happens in a worker. Previously this was only enabled when theESBUILD_WORKER_THREADSenvironment variable was set to1. But this experiment has been available for a while (since version 0.9.6) without any reported issues. Now this hack will be enabled by default. It can be disabled by settingESBUILD_WORKER_THREADSto0before running node. -
Fix nested output directories with WebAssembly on Windows (#1399)
Many functions in Go's standard library have a bug where they do not work on Windows when using Go with WebAssembly. This is a long-standing bug and is a fault with the design of the standard library, so it's unlikely to be fixed. Basically Go's standard library is designed to bake "Windows or not" decision into the compiled executable, but WebAssembly is platform-independent which makes "Windows or not" is a run-time decision instead of a compile-time decision. Oops.
I have been working around this by trying to avoid using path-related functions in the Go standard library and doing all path manipulation by myself instead. This involved completely replacing Go's
path/filepathlibrary. However, I missed theos.MkdirAllfunction which is also does path manipulation but is outside of thepath/filepathpackage. This meant that nested output directories failed to be created on Windows, which caused a build error. This problem only affected theesbuild-wasmpackage.This release manually reimplements nested output directory creation to work around this bug in the Go standard library. So nested output directories should now work on Windows with the
esbuild-wasmpackage.
-
Add a target for ES2021
It's now possible to use
--target=es2021to target the newly-released JavaScript version ES2021. The only difference between that and--target=es2020is that logical assignment operators such asa ||= bare not converted to regular assignment operators such asa || (a = b). -
Minify the syntax
Infinityto1 / 0(#1385)The
--minify-syntaxflag (automatically enabled by--minify) will now minify the expressionInfinityto1 / 0, which uses fewer bytes:// Original code const a = Infinity; // Output with "--minify-syntax" const a = 1 / 0;This change was contributed by @Gusted.
-
Minify syntax in the CSS
transformproperty (#1390)This release includes various size reductions for CSS transform matrix syntax when minification is enabled:
/* Original code */ div { transform: translate3d(0, 0, 10px) scale3d(200%, 200%, 1) rotate3d(0, 0, 1, 45deg); } /* Output with "--minify-syntax" */ div { transform: translateZ(10px) scale(2) rotate(45deg); }The
translate3dtotranslateZconversion was contributed by @steambap. -
Support for the case-sensitive flag in CSS attribute selectors (#1397)
You can now use the case-sensitive CSS attribute selector flag
ssuch as in[type="a" s] { list-style: lower-alpha; }. Previously doing this caused a warning about unrecognized syntax.
-
Allow
thiswith--define(#1361)You can now override the default value of top-level
thiswith the--definefeature. Top-levelthisdefaults to beingundefinedin ECMAScript modules andexportsin CommonJS modules. For example:// Original code ((obj) => { ... })(this); // Output with "--define:this=window" ((obj) => { ... })(window);Note that overriding what top-level
thisis will likely break code that uses it correctly. So this new feature is only useful in certain cases. -
Fix CSS minification issue with
!importantand duplicate declarations (#1372)Previously CSS with duplicate declarations for the same property where the first one was marked with
!importantwas sometimes minified incorrectly. For example:.selector { padding: 10px !important; padding: 0; }This was incorrectly minified as
.selector{padding:0}. The bug affected three properties:padding,margin, andborder-radius. With this release, this code will now be minified as.selector{padding:10px!important;padding:0}instead which means there is no longer a difference between minified and non-minified code in this case.
-
Plugins can now specify
sideEffects: false(#1009)The default path resolution behavior in esbuild determines if a given file can be considered side-effect free (in the Webpack-specific sense) by reading the contents of the nearest enclosing
package.jsonfile and looking for"sideEffects": false. However, up until now this was impossible to achieve in an esbuild plugin because there was no way of returning this metadata back to esbuild.With this release, esbuild plugins can now return
sideEffects: falseto mark a file as having no side effects. Here's an example:esbuild.build({ entryPoints: ['app.js'], bundle: true, plugins: [{ name: 'env-plugin', setup(build) { build.onResolve({ filter: /^env$/ }, args => ({ path: args.path, namespace: 'some-ns', sideEffects: false, })) build.onLoad({ filter: /.*/, namespace: 'some-ns' }, () => ({ contents: `export default self.env || (self.env = getEnv())`, })) }, }], })This plugin creates a virtual module that can be generated by importing the string
env. However, since the plugin returnssideEffects: false, the generated virtual module will not be included in the bundle if all of the imported values from the moduleenvend up being unused.This feature was contributed by @chriscasola.
-
Remove a warning about unsupported source map comments (#1358)
This removes a warning that indicated when a source map comment couldn't be supported. Specifically, this happens when you enable source map generation and esbuild encounters a file with a source map comment pointing to an external file but doesn't have enough information to know where to look for that external file (basically when the source file doesn't have an associated directory to use for path resolution). In this case esbuild can't respect the input source map because it cannot be located. The warning was annoying so it has been removed. Source maps still won't work, however.
-
Quote object properties that are modern Unicode identifiers (#1349)
In ES6 and above, an identifier is a character sequence starting with a character in the
ID_StartUnicode category and followed by zero or more characters in theID_ContinueUnicode category, and these categories must be drawn from Unicode version 5.1 or above.But in ES5, an identifier is a character sequence starting with a character in one of the
Lu, Ll, Lt, Lm, Lo, NlUnicode categories and followed by zero or more characters in theLu, Ll, Lt, Lm, Lo, Nl, Mn, Mc, Nd, PcUnicode categories, and these categories must be drawn from Unicode version 3.0 or above.Previously esbuild always used the ES6+ identifier validation test when deciding whether to use an identifier or a quoted string to encode an object property but with this release, it will use the ES5 validation test instead:
// Original code x.ꓷꓶꓲꓵꓭꓢꓱ = { ꓷꓶꓲꓵꓭꓢꓱ: y }; // Old output x.ꓷꓶꓲꓵꓭꓢꓱ = { ꓷꓶꓲꓵꓭꓢꓱ: y }; // New output x["ꓷꓶꓲꓵꓭꓢꓱ"] = { "ꓷꓶꓲꓵꓭꓢꓱ": y };This approach should ensure maximum compatibility with all JavaScript environments that support ES5 and above. Note that this means minified files containing Unicode properties may be slightly larger than before.
-
Ignore
tsconfig.jsonfiles insidenode_modules(#1355)Package authors often publish their
tsconfig.jsonfiles to npm because of npm's default-include publishing model and because these authors probably don't know about.npmignorefiles. People trying to use these packages with esbuild have historically complained that esbuild is respectingtsconfig.jsonin these cases. The assumption is that the package author published these files by accident.With this release, esbuild will no longer respect
tsconfig.jsonfiles when the source file is inside anode_modulesfolder. Note thattsconfig.jsonfiles insidenode_modulesare still parsed, and extendingtsconfig.jsonfiles from inside a package is still supported. -
Fix missing
--metafilewhen using--watch(#1357)Due to an oversight, the
--metafilesetting didn't work when--watchwas also specified. This only affected the command-line interface. With this release, the--metafilesetting should now work in this case. -
Add a hidden
__esModuleproperty to modules in ESM format (#1338)Module namespace objects from ESM files will now have a hidden
__esModuleproperty. This improves compatibility with code that has been converted from ESM syntax to CommonJS by Babel or TypeScript. For example:// Input TypeScript code import x from "y" console.log(x) // Output JavaScript code from the TypeScript compiler var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const y_1 = __importDefault(require("y")); console.log(y_1.default);If the object returned by
require("y")doesn't have an__esModuleproperty, theny_1will be the object{ "default": require("y") }. If the file"y"is in ESM format and has a default export of, say, the valuenull, that meansy_1will now be{ "default": { "default": null } }and you will need to usey_1.default.defaultto access the default value. Adding an automatically-generated__esModuleproperty when converting files in ESM format to CommonJS is required to make this code work correctly (i.e. for the value to be accessible via justy_1.defaultinstead).With this release, code in ESM format will now have an automatically-generated
__esModuleproperty to satisfy this convention. The property is non-enumerable so it shouldn't show up when iterating over the properties of the object. As a result, the export name__esModuleis now reserved for use with esbuild. It's now an error to create an export with the name__esModule.This fix was contributed by @lbwa.
-
Improve template literal lowering transformation conformance (#1327)
This release contains the following improvements to template literal lowering for environments that don't support tagged template literals natively (such as
--target=es5):-
For tagged template literals, the arrays of strings that are passed to the tag function are now frozen and immutable. They are also now cached so they should now compare identical between multiple template evaluations:
// Original code console.log(tag`\u{10000}`) // Old output console.log(tag(__template(["𐀀"], ["\\u{10000}"]))); // New output var _a; console.log(tag(_a || (_a = __template(["𐀀"], ["\\u{10000}"])))); -
For tagged template literals, the generated code size is now smaller in the common case where there are no escape sequences, since in that case there is no distinction between "raw" and "cooked" values:
// Original code console.log(tag`some text without escape sequences`) // Old output console.log(tag(__template(["some text without escape sequences"], ["some text without escape sequences"]))); // New output var _a; console.log(tag(_a || (_a = __template(["some text without escape sequences"])))); -
For non-tagged template literals, the generated code now uses chains of
.concat()calls instead of string addition:// Original code console.log(`an ${example} template ${literal}`) // Old output console.log("an " + example + " template " + literal); // New output console.log("an ".concat(example, " template ").concat(literal));The old output was incorrect for several reasons including that
toStringmust be called instead ofvalueOffor objects and that passing aSymbolinstance should throw instead of converting the symbol to a string. Using.concat()instead of string addition fixes both of those correctness issues. And you can't use a single.concat()call because side effects must happen inline instead of at the end.
-
-
Only respect
targetintsconfig.jsonwhen esbuild's target is not configured (#1332)In version 0.12.4, esbuild began respecting the
targetsetting intsconfig.json. However, sometimestsconfig.jsoncontains target values that should not be used. With this release, esbuild will now only use thetargetvalue intsconfig.jsonas the language level when esbuild'stargetsetting is not configured. If esbuild'stargetsetting is configured then thetargetvalue intsconfig.jsonis now ignored. -
Fix the order of CSS imported from JS (#1342)
Importing CSS from JS when bundling causes esbuild to generate a sibling CSS output file next to the resulting JS output file containing the bundled CSS. The order of the imported CSS files in the output was accidentally the inverse order of the order in which the JS files were evaluated. Instead the order of the imported CSS files should match the order in which the JS files were evaluated. This fix was contributed by @dmitrage.
-
Fix an edge case with transforming
export default class(#1346)Statements of the form
export default class x {}were incorrectly transformed toclass x {} var y = x; export {y as default}instead ofclass x {} export {x as default}. Transforming these statements like this is incorrect in the rare case that the class is later reassigned by name within the same file such asexport default class x {} x = null. Here the imported value should benullbut was incorrectly the class object instead. This is unlikely to matter in real-world code but it has still been fixed to improve correctness.
-
Add support for lowering tagged template literals to ES5 (#297)
This release adds support for lowering tagged template literals such as
String.raw`\unicode`to target environments that don't support them such as--target=es5(non-tagged template literals were already supported). Each literal turns into a function call to a helper function:// Original code console.log(String.raw`\unicode`) // Lowered code console.log(String.raw(__template([void 0], ["\\unicode"]))); -
Change class field behavior to match TypeScript 4.3
TypeScript 4.3 includes a subtle breaking change that wasn't mentioned in the TypeScript 4.3 blog post: class fields will now be compiled with different semantics if
"target": "ESNext"is present intsconfig.json. Specifically in this caseuseDefineForClassFieldswill default totruewhen not specified instead offalse. This means class field behavior in TypeScript code will now match JavaScript instead of doing something else:class Base { set foo(value) { console.log('set', value) } } class Derived extends Base { foo = 123 } new Derived()In TypeScript 4.2 and below, the TypeScript compiler would generate code that prints
set 123whentsconfig.jsoncontains"target": "ESNext"but in TypeScript 4.3, the TypeScript compiler will now generate code that doesn't print anything. This is the difference between "assign" semantics and "define" semantics. With this release, esbuild has been changed to follow the TypeScript 4.3 behavior. -
Avoid generating the character sequence
</script>(#1322)If the output of esbuild is inlined into a
<script>...</script>tag inside an HTML file, the character sequence</script>inside the JavaScript code will accidentally cause the script tag to be terminated early. There are at least four such cases where this can happen:console.log('</script>') console.log(1</script>/.exec(x).length) console.log(String.raw`</script>`) // @license </script>With this release, esbuild will now handle all of these cases and avoid generating the problematic character sequence:
console.log('<\/script>'); console.log(1< /script>/.exec(x).length); console.log(String.raw(__template(["<\/script>"], ["<\/script>"]))); // @license <\/script> -
Change the triple-slash reference comment for Deno (#1325)
The comment in esbuild's JavaScript API implementation for Deno that references the TypeScript type declarations has been changed from
/// <reference path="./mod.d.ts" />to/// <reference types="./mod.d.ts" />. This comment was copied from Deno's documentation but apparently Deno's documentation was incorrect. The comment in esbuild's Deno bundle has been changed to reflect Deno's latest documentation.
-
Reorder name preservation before TypeScript decorator evaluation (#1316)
The
--keep-namesoption ensures the.nameproperty on functions and classes remains the same after bundling. However, this was being enforced after TypeScript decorator evaluation which meant that the decorator could observe the incorrect name. This has been fixed and now.namepreservation happens before decorator evaluation instead. -
Potential fix for a determinism issue (#1304)
This release contains a potential fix for an unverified issue with non-determinism in esbuild. The regression was apparently introduced in 0.11.13 and may be related to parallelism that was introduced around the point where dynamic
import()expressions are added to the list of entry points. Hopefully this fix should resolve the regression. -
Respect
targetintsconfig.json(#277)Each JavaScript file that esbuild bundles will now be transformed according to the
targetlanguage level from the nearest enclosingtsconfig.jsonfile. This is in addition to esbuild's own--targetsetting; the two settings are merged by transforming any JavaScript language feature that is unsupported in either esbuild's configured--targetvalue or thetargetproperty in thetsconfig.jsonfile.
-
Ensure JSX element names start with a capital letter (#1309)
The JSX specification only describes the syntax and says nothing about how to interpret it. But React (and therefore esbuild) treats JSX tags that start with a lower-case ASCII character as strings instead of identifiers. That way the tag
<i/>always refers to the italic HTML elementiand never to a local variable namedi.However, esbuild may rename identifiers for any number of reasons such as when minification is enabled. Previously esbuild could sometimes rename identifiers used as tag names such that they start with a lower-case ASCII character. This is problematic when JSX syntax preservation is enabled since subsequent JSX processing would then turn these identifier references into strings.
With this release, esbuild will now make sure identifiers used in tag names start with an upper-case ASCII character instead when JSX syntax preservation is enabled. This should avoid problems when using esbuild with JSX transformation tools.
-
Fix a single hyphen being treated as a CSS name (#1310)
CSS identifiers are allowed to start with a
-character if (approximately) the following character is a letter, an escape sequence, a non-ASCII character, the character_, or another-character. This check is used in certain places when printing CSS to determine whether a token is a valid identifier and can be printed as such or whether it's an invalid identifier and needs to be quoted as a string. One such place is in attribute selectors such as[a*=b].However, esbuild had a bug where a single
-character was incorrectly treated as a valid identifier in this case. This is because the end of string became U+FFFD (the Unicode replacement character) which is a non-ASCII character and a valid name-start code point. With this release a single-character is no longer treated as a valid identifier. This fix was contributed by @lbwa.
-
Fix various code generation and minification issues (#1305)
This release fixes the following issues, which were all identified by running esbuild against the latest UglifyJS test suite:
-
The
inoperator is now surrounded parentheses inside arrow function expression bodies insideforloop initializers:// Original code for ((x => y in z); 0; ) ; // Old output for ((x) => y in z; 0; ) ; // New output for ((x) => (y in z); 0; ) ;Without this, the
inoperator would cause the for loop to be considered a for-in loop instead. -
The statement
return undefined;is no longer minified toreturn;inside async generator functions:// Original code return undefined; // Old output return; // New output return void 0;Using
return undefined;inside an async generator function has the same effect asreturn await undefined;which schedules a task in the event loop and runs code in a different order than justreturn;, which doesn't hide an implicitawaitexpression. -
Property access expressions are no longer inlined in template tag position:
// Original code (null, a.b)``, (null, a[b])``; // Old output a.b``, a[b]``; // New output (0, a.b)``, (0, a[b])``;The expression
a.b`c`is different than the expression(0, a.b)`c`. The first calls the functiona.bwithaas the value forthisbut the second calls the functiona.bwith the default value forthis(the global object in non-strict mode orundefinedin strict mode). -
Verbatim
__proto__properties inside object spread are no longer inlined when minifying:// Original code x = { ...{ __proto__: { y: true } } }.y; // Old output x = { __proto__: { y: !0 } }.y; // New output x = { ...{ __proto__: { y: !0 } } }.y;A verbatim (i.e. non-computed non-method) property called
__proto__inside an object literal actually sets the prototype of the surrounding object literal. It does not add an "own property" called__proto__to that object literal, so inlining it into the parent object literal would be incorrect. The presence of a__proto__property now stops esbuild from applying the object spread inlining optimization when minifying. -
The value of
thishas now been fixed for lowered private class members that are used as template tags:// Original code x = (new (class { a = this.#c``; b = 1; #c() { return this } })).a.b; // Old output var _c, c_fn, _a; x = new (_a = class { constructor() { __privateAdd(this, _c); __publicField(this, "a", __privateMethod(this, _c, c_fn)``); __publicField(this, "b", 1); } }, _c = new WeakSet(), c_fn = function() { return this; }, _a)().a.b; // New output var _c, c_fn, _a; x = new (_a = class { constructor() { __privateAdd(this, _c); __publicField(this, "a", __privateMethod(this, _c, c_fn).bind(this)``); __publicField(this, "b", 1); } }, _c = new WeakSet(), c_fn = function() { return this; }, _a)().a.b;The value of
thishere should be an instance of the class because the template tag is a property access expression. However, it was previously the default value (the global object in non-strict mode orundefinedin strict mode) instead due to the private member transformation, which is incorrect. -
Invalid escape sequences are now allowed in tagged template literals
This implements the template literal revision feature: https://github.com/tc39/proposal-template-literal-revision. It allows you to process tagged template literals using custom semantics that don't follow JavaScript escape sequence rules without causing a syntax error:
console.log((x => x.raw)`invalid \unicode escape sequence`)
-
-
Add the ability to preserve JSX syntax (#735)
You can now pass
--jsx=preserveto esbuild to prevent JSX from being transformed into JS. Instead, JSX syntax in all input files is preserved throughout the pipeline and is printed as JSX syntax in the generated output files. Note that this means the output files are no longer valid JavaScript code if you enable this setting. This feature is intended to be used when you want to transform the JSX syntax in esbuild's output files by another tool after bundling, usually one with a different JSX-to-JS transform than the one esbuild implements. -
Update the list of built-in node modules (#1294)
The list of built-in modules that come with node was outdated, so it has been updated. It now includes new modules such as
wasiand_http_common. Modules in this list are automatically marked as external when esbuild's platform is configured tonode.
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
@importrule in CSS as being replaced by the contents of that file similar to#includein C/C++. However, this is incorrect in the case of@importcycles because it would cause infinite expansion. A more accurate way to imagine this is that in CSS, a file is evaluated at the last@importlocation while in JS, a file is evaluated at the firstimportlocation.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
@importmultiple 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.csswill 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.cssis evaluated at the location of the last@importinstead of the first@import. The fix for this case is to remove the nested imports ofreset.cssand to importreset.cssexactly 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@importrules for each CSS file imported into a JS file. These@importrules 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
importstatements. 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@importorder 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
reactnpm 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_ENVwith"production"and "inject" to replaceprocesswith a shim that emulates node's process API, thenprocesswas previously replaced first and thenprocess.env.NODE_ENVwasn't matched becauseprocessreferred 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_ENVwill 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_COLORenvironment variableThe CLI will now omit color if the
NO_COLORenvironment variable is present, which is an existing convention that is followed by some other software. See https://no-color.org/ for more information.
-
Add a shim function for unbundled uses of
require(#1202)Modules in CommonJS format automatically get three variables injected into their scope:
module,exports, andrequire. These allow the code to import other modules and to export things from itself. The bundler automatically rewrites uses ofmoduleandexportsto refer to the module's exports and certain uses ofrequireto a helper function that loads the imported module.Not all uses of
requirecan be converted though, and un-converted uses ofrequirewill end up in the output. This is problematic becauserequireis only present at run-time if the output is run as a CommonJS module. Otherwiserequireis undefined, which means esbuild's behavior is inconsistent between compile-time and run-time. Themoduleandexportsvariables are objects at compile-time and run-time butrequireis a function at compile-time and undefined at run-time. This causes code that checks fortypeof requireto have inconsistent behavior:if (typeof require === 'function' && typeof exports === 'object' && typeof module === 'object') { console.log('CommonJS detected') }In the above example, ideally
CommonJS detectedwould always be printed since the code is being bundled with a CommonJS-aware bundler. To fix this, esbuild will now substitute references torequirewith a stub__requirefunction when bundling if the output format is something other than CommonJS. This should ensure thatrequireis now consistent between compile-time and run-time. When bundled, code that uses unbundled references torequirewill now look something like this:var __require = (x) => { if (typeof require !== "undefined") return require(x); throw new Error('Dynamic require of "' + x + '" is not supported'); }; var __commonJS = (cb, mod) => () => (mod || cb((mod = {exports: {}}).exports, mod), mod.exports); var require_example = __commonJS((exports, module) => { if (typeof __require === "function" && typeof exports === "object" && typeof module === "object") { console.log("CommonJS detected"); } }); require_example(); -
Fix incorrect caching of internal helper function library (#1292)
This release fixes a bug where running esbuild multiple times with different configurations sometimes resulted in code that would crash at run-time. The bug was introduced in version 0.11.19 and happened because esbuild's internal helper function library is parsed once and cached per configuration, but the new profiler name option was accidentally not included in the cache key. This option is now included in the cache key so this bug should now be fixed.
-
Minor performance improvements
This release contains some small performance improvements to offset an earlier minor performance regression due to the addition of certain features such as hashing for entry point files. The benchmark times on the esbuild website should now be accurate again (versions of esbuild after the regression but before this release were slightly slower than the benchmark).
-
Add support for the "import assertions" proposal
This is new JavaScript syntax that was shipped in Chrome 91. It looks like this:
import './foo.json' assert { type: 'json' } import('./bar.json', { assert: { type: 'json' } })On the web, the content type for a given URL is determined by the
Content-TypeHTTP header instead of the file extension. So adding support for importing non-JS content types such as JSON to the web could cause security issues since importing JSON from an untrusted source is safe while importing JS from an untrusted source is not.Import assertions are a new feature to address this security concern and unblock non-JS content types on the web. They cause the import to fail if the
Content-Typeheader doesn't match the expected value. This prevents security issues for data-oriented content types such as JSON since it guarantees that data-oriented content will never accidentally be evaluated as code instead of data. More information about the proposal is available here: https://github.com/tc39/proposal-import-assertions.This release includes support for parsing and printing import assertions. They will be printed if the configured target environment supports them (currently only in
esnextandchrome91), otherwise they will be omitted. If they aren't supported in the configured target environment and it's not possible to omit them, which is the case for certain dynamicimport()expressions, then using them is a syntax error. Import assertions are otherwise unused by the bundler. -
Forbid the token sequence
for ( async ofwhen not followed by=>This follows a recently-fixed ambiguity in the JavaScript specification, which you can read about here: https://github.com/tc39/ecma262/pull/2256. Prior to this change in the specification, it was ambiguous whether this token sequence should be parsed as
for ( async of =>orfor ( async of ;. V8 and esbuild expected=>afterfor ( async ofwhile SpiderMonkey and JavaScriptCore did something else.The ambiguity has been removed and the token sequence
for ( async ofis now forbidden by the specification when not followed by=>, so esbuild now forbids this as well. Note that the token sequencefor await (async ofis still allowed even when not followed by=>. Code such asfor ((async) of []) ;is still allowed and will now be printed with parentheses to avoid the grammar ambiguity. -
Restrict
superproperty access to inside of methodsYou can now only use
super.xandsuper[x]expressions inside of methods. Previously these expressions were incorrectly allowed everywhere. This means esbuild now follows the JavaScript language specification more closely.
-
TypeScript
overridefor parameter properties (#1262)You can now use the
overridekeyword instead of or in addition to thepublic,private,protected, andreadonlykeywords for declaring a TypeScript parameter property:class Derived extends Base { constructor(override field: any) { } }This feature was recently added to the TypeScript compiler and will presumably be in an upcoming version of the TypeScript language. Support for this feature in esbuild was contributed by @g-plane.
-
Fix duplicate export errors due to TypeScript import-equals statements (#1283)
TypeScript has a special import-equals statement that is not part of JavaScript. It looks like this:
import a = foo.a import b = a.b import c = b.c import x = foo.x import y = x.y import z = y.z export let bar = cEach import can be a type or a value and type-only imports need to be eliminated when converting this code to JavaScript, since types do not exist at run-time. The TypeScript compiler generates the following JavaScript code for this example:
var a = foo.a; var b = a.b; var c = b.c; export let bar = c;The
x,y, andzimport statements are eliminated in esbuild by iterating over imports and exports multiple times and continuing to remove unused TypeScript import-equals statements until none are left. The first pass removeszand marksyas unused, the second pass removesyand marksxas unused, and the third pass removesx.However, this had the side effect of making esbuild incorrectly think that a single export is exported twice (because it's processed more than once). This release fixes that bug by only iterating multiple times over imports, not exports. There should no longer be duplicate export errors for this case.
-
Add support for type-only TypeScript import-equals statements (#1285)
This adds support for the following new TypeScript syntax that was added in version 4.2:
import type React = require('react')Unlike
import React = require('react'), this statement is a type declaration instead of a value declaration and should be omitted from the generated code. See microsoft/TypeScript#41573 for details. This feature was contributed by @g-plane.
-
Omit warning about duplicate JSON keys from inside
node_modules(#1254)This release no longer warns about duplicate keys inside
package.jsonfiles insidenode_modules. There are packages like this that are published to npm, and this warning is unactionable. Now esbuild will only issue this warning outside ofnode_modulesdirectories. -
Add CSS minification for
box-shadowvaluesThe CSS
box-shadowproperty is now minified when--mangle-syntaxis enabled. This includes trimming length values and minifying color representations. -
Fix object spread transform for non-spread getters (#1259)
When transforming an object literal containing object spread (the
...syntax), properties inside the spread should be evaluated but properties outside the spread should not be evaluated. Previously esbuild's object spread transform incorrectly evaluated properties in both cases. Consider this example:var obj = { ...{ get x() { console.log(1) } }, get y() { console.log(3) }, } console.log(2) obj.yThis should print out
1 2 3because the non-spread getter should not be evaluated. Instead, esbuild was incorrectly transforming this into code that printed1 3 2. This issue should now be fixed with this release. -
Prevent private class members from being added more than once
This fixes a corner case with the private class member implementation. Constructors in JavaScript can return an object other than
this, so private class members can actually be added to objects other thanthis. This can be abused to attach completely private metadata to other objects:class Base { constructor(x) { return x } } class Derived extends Base { #y static is(z) { return #y in z } } const foo = {} new Derived(foo) console.log(Derived.is(foo)) // trueThis already worked in code transformed by esbuild for older browsers. However, calling
new Derived(foo)multiple times in the above code was incorrectly allowed. This should not be allowed because it would mean that the private field#ywould be re-declared. This is no longer allowed starting from this release.
-
Allow esbuild to be restarted in Deno (#1238)
The esbuild API for Deno has an extra function called
stop()that doesn't exist in esbuild's API for node. This is because Deno doesn't provide a way to stop esbuild automatically, so callingstop()is required to allow Deno to exit. However, once stopped the esbuild API could not be restarted.With this release, you can now continue to use esbuild after calling
stop(). This will restart esbuild's API and means that you will need to callstop()again for Deno to be able to exit. This feature was contributed by @lucacasonato. -
Fix code splitting edge case (#1252)
This release fixes an edge case where bundling with code splitting enabled generated incorrect code if multiple ESM entry points re-exported the same re-exported symbol from a CommonJS file. In this case the cross-chunk symbol dependency should be the variable that holds the return value from the
require()call instead of the original ESM namedimportclause item. When this bug occurred, the generated ESM code contained an export and import for a symbol that didn't exist, which caused a module initialization error. This case should now work correctly. -
Fix code generation with
declareclass fields (#1242)This fixes a bug with TypeScript code that uses
declareon a class field and yourtsconfig.jsonfile has"useDefineForClassFields": true. Fields marked asdeclareshould not be defined in the generated code, but they were incorrectly being declared asundefined. These fields are now correctly omitted from the generated code. -
Annotate module wrapper functions in debug builds (#1236)
Sometimes esbuild needs to wrap certain modules in a function when bundling. This is done both for lazy evaluation and for CommonJS modules that use a top-level
returnstatement. Previously these functions were all anonymous, so stack traces for errors thrown during initialization looked like this:Error: Electron failed to install correctly, please delete node_modules/electron and try installing again at getElectronPath (out.js:16:13) at out.js:19:21 at out.js:1:45 at out.js:24:3 at out.js:1:45 at out.js:29:3 at out.js:1:45 at Object.<anonymous> (out.js:33:1)This release adds names to these anonymous functions when minification is disabled. The above stack trace now looks like this:
Error: Electron failed to install correctly, please delete node_modules/electron and try installing again at getElectronPath (out.js:19:15) at node_modules/electron/index.js (out.js:22:23) at __require (out.js:2:44) at src/base/window.js (out.js:29:5) at __require (out.js:2:44) at src/base/kiosk.js (out.js:36:5) at __require (out.js:2:44) at Object.<anonymous> (out.js:41:1)This is similar to Webpack's development-mode behavior:
Error: Electron failed to install correctly, please delete node_modules/electron and try installing again at getElectronPath (out.js:23:11) at Object../node_modules/electron/index.js (out.js:27:18) at __webpack_require__ (out.js:96:41) at Object../src/base/window.js (out.js:49:1) at __webpack_require__ (out.js:96:41) at Object../src/base/kiosk.js (out.js:38:1) at __webpack_require__ (out.js:96:41) at out.js:109:1 at out.js:111:3 at Object.<anonymous> (out.js:113:12)These descriptive function names will additionally be available when using a profiler such as the one included in the "Performance" tab in Chrome Developer Tools. Previously all functions were named
(anonymous)which made it difficult to investigate performance issues during bundle initialization. -
Add CSS minification for more cases
The following CSS minification cases are now supported:
-
The CSS
marginproperty family is now minified including combining themargin-top,margin-right,margin-bottom, andmargin-leftproperties into a singlemarginproperty. -
The CSS
paddingproperty family is now minified including combining thepadding-top,padding-right,padding-bottom, andpadding-leftproperties into a singlepaddingproperty. -
The CSS
border-radiusproperty family is now minified including combining theborder-top-left-radius,border-top-right-radius,border-bottom-right-radius, andborder-bottom-left-radiusproperties into a singleborder-radiusproperty. -
The four special pseudo-elements
::before,::after,::first-line, and::first-letterare allowed to be parsed with one:for legacy reasons, so the::is now converted to:for these pseudo-elements. -
Duplicate CSS rules are now deduplicated. Only the last rule is kept, since that's the only one that has any effect. This applies for both top-level rules and nested rules.
-
-
Preserve quotes around properties when minification is disabled (#1251)
Previously the parser did not distinguish between unquoted and quoted properties, since there is no semantic difference. However, some tools such as Google Closure Compiler with "advanced mode" enabled attach their own semantic meaning to quoted properties, and processing code intended for Google Closure Compiler's advanced mode with esbuild was changing those semantics. The distinction between unquoted and quoted properties is now made in the following cases:
import * as ns from 'external-pkg' console.log([ { x: 1, 'y': 2 }, { x() {}, 'y'() {} }, class { x = 1; 'y' = 2 }, class { x() {}; 'y'() {} }, { x: x, 'y': y } = z, [x.x, y['y']], [ns.x, ns['y']], ])The parser will now preserve the quoted properties in these cases as long as
--minify-syntaxis not enabled. This does not mean that esbuild is officially supporting Google Closure Compiler's advanced mode, just that quoted properties are now preserved when the AST is pretty-printed. Google Closure Compiler's advanced mode accepts a language that shares syntax with JavaScript but that deviates from JavaScript semantics and there could potentially be other situations where preprocessing code intended for Google Closure Compiler's advanced mode with esbuild first causes it to break. If that happens, that is not a bug with esbuild.
-
Add support for OpenBSD on x86-64 (#1235)
Someone has asked for OpenBSD to be supported on x86-64. It should now be supported starting with this release.
-
Fix an incorrect warning about top-level
thisThis was introduced in the previous release, and happens when using a top-level
asyncarrow function with a compilation target that doesn't support it. The reason is that doing this generates a shim that preserves the value ofthis. However, this warning message is confusing because there is not necessarily anythispresent in the source code. The warning message has been removed in this case. Now it should only show up ifthisis actually present in the source code.
-
Fix building with a large
stdinstring with Deno (#1219)When I did the initial port of esbuild's node-based API to Deno, I didn't realize that Deno's
write(bytes)function doesn't actually write the provided bytes. Instead it may only write some of those bytes and needs to be repeatedly called again until it writes everything. This meant that calling esbuild's Deno-based API could hang if the API request was large enough, which can happen in practice when using thestdinstring feature. ThewriteAPI is now called in a loop so these hangs in Deno should now be fixed. -
Add a warning about replacing
thiswithundefinedin ESM code (#1225)There is existing JavaScript code that sometimes references top-level
thisas a way to access the global scope. However, top-levelthisis actually specified to beundefinedinside of ECMAScript module code, which makes referencing top-levelthisinside ESM code useless. This issue can come up when the existing JavaScript code is adapted for ESM by addingimportand/orexport. All top-level references tothisare replaced withundefinedwhen bundling to make sure ECMAScript module behavior is emulated correctly regardless of the environment in which the resulting code is run.With this release, esbuild will now warn about this when bundling:
> example.mjs:1:61: warning: Top-level "this" will be replaced with undefined since this file is an ECMAScript module 1 │ export let Array = (typeof window !== 'undefined' ? window : this).Array ╵ ~~~~ example.mjs:1:0: note: This file is considered an ECMAScript module because of the "export" keyword here 1 │ export let Array = (typeof window !== 'undefined' ? window : this).Array ╵ ~~~~~~This warning is not unique to esbuild. Rollup also already has a similar warning:
(!) `this` has been rewritten to `undefined` https://rollupjs.org/guide/en/#error-this-is-undefined example.mjs 1: export let Array = (typeof window !== 'undefined' ? window : this).Array ^ -
Allow a string literal as a JSX fragment (#1217)
TypeScript's JSX implementation allows you to configure a custom JSX factory and a custom JSX fragment, but requires that they are both valid JavaScript identifier member expression chains. Since esbuild's JSX implementation is based on TypeScript, esbuild has the same requirement. So
React.createElementis a valid JSX factory value but['React', 'createElement']is not.However, the Mithril framework has decided to use
"["as a JSX fragment, which is not a valid JavaScript identifier member expression chain. This meant that using Mithril with esbuild required a workaround. In this release, esbuild now lets you use a string literal as a custom JSX fragment. It should now be easier to use esbuild's JSX implementation with libraries such as Mithril. -
Fix
metafileinonEndwithwatchmode enabled (#1186)This release fixes a bug where the
metafileproperty was incorrectly undefined inside pluginonEndcallbacks ifwatchmode is enabled for all builds after the first build. Themetafileproperty was accidentally being set after callingonEndinstead of before.
-
Fix TypeScript
enumedge case (#1198)In TypeScript, you can reference the inner closure variable in an
enumwithin the inner closure by name:enum A { B = A }The TypeScript compiler generates the following code for this case:
var A; (function (A) { A[A["B"] = A] = "B"; })(A || (A = {}));However, TypeScript also lets you declare an
enumvalue with the same name as the inner closure variable. In that case, the value "shadows" the declaration of the inner closure variable:enum A { A = 1, B = A }The TypeScript compiler generates the following code for this case:
var A; (function (A) { A[A["A"] = 1] = "A"; A[A["B"] = 1] = "B"; })(A || (A = {}));Previously esbuild reported a duplicate variable declaration error in the second case due to the collision between the
enumvalue and the inner closure variable with the same name. With this release, the shadowing is now handled correctly. -
Parse the
@-moz-documentCSS rule (#1203)This feature has been removed from the web because it's actively harmful, at least according to this discussion. However, there is one exception where
@-moz-document url-prefix() {is accepted by Firefox to basically be an "if Firefox" conditional rule. Because of this, esbuild now parses the@-moz-documentCSS rule. This should result in better pretty-printing and minification and no more warning when this rule is used. -
Fix syntax error in TypeScript-specific speculative arrow function parsing (#1211)
Because of grammar ambiguities, expressions that start with a parenthesis are parsed using what's called a "cover grammar" that is a super-position of both a parenthesized expression and an arrow function parameter list. In JavaScript, the cover grammar is unambiguously an arrow function if and only if the following token is a
=>token.But in TypeScript, the expression is still ambiguously a parenthesized expression or an arrow function if the following token is a
:since it may be the second half of the?:operator or a return type annotation. This requires speculatively attempting to reduce the cover grammar to an arrow function parameter list.However, when doing this esbuild eagerly reported an error if a default argument was encountered and the target is
es5(esbuild doesn't support lowering default arguments to ES5). This is problematic in the following TypeScript code since the parenthesized code turns out to not be an arrow function parameter list:function foo(check, hover) { return check ? (hover = 2, bar) : baz(); }Previously this code incorrectly generated an error since
hover = 2was incorrectly eagerly validated as a default argument. With this release, the reporting of the default argument error when targetinges5is now done lazily and only when it's determined that the parenthesized code should actually be interpreted as an arrow function parameter list. -
Further changes to the behavior of the
browserfield (#1209)This release includes some changes to how the
browserfield inpackage.jsonis interpreted to better match how Browserify, Webpack, Parcel, and Rollup behave. The interpretation of this map in esbuild is intended to be applied if and only if it's applied by any one of these bundlers. However, there were some cases where esbuild applied the mapping and none of the other bundlers did, which could lead to build failures. These cases have been added to my growing list ofbrowserfield test cases and esbuild's behavior should now be consistent with other bundlers again. -
Avoid placing a
super()call inside areturnstatement (#1208)When minification is enabled, an expression followed by a return statement (e.g.
a(); return b) is merged into a single statement (e.g.return a(), b). This is done because it sometimes results in smaller code. If the return statement is the only statement in a block and the block is in a single-statement context, the block can be removed which saves a few characters.Previously esbuild applied this rule to calls to
super()inside of constructors. Doing that broke esbuild's class lowering transform that tries to insert class field initializers after thesuper()call. This transform isn't robust and only scans the top-level statement list inside the constructor, so inserting thesuper()call inside of thereturnstatement means class field initializers were inserted before thesuper()call instead of after. This could lead to run-time crashes due to initialization failure.With this release, top-level calls to
super()will no longer be placed insidereturnstatements (in addition to various other kinds of statements such asthrow, which are now also handled). This should avoid class field initializers being inserted before thesuper()call. -
Fix a bug with
onEndand watch mode (#1186)This release fixes a bug where
onEndplugin callbacks only worked with watch mode when anonRebuildwatch mode callback was present. NowonEndcallbacks should fire even if there is noonRebuildcallback. -
Fix an edge case with minified export names and code splitting (#1201)
The names of symbols imported from other chunks were previously not considered for renaming during minified name assignment. This could cause a syntax error due to a name collision when two symbols have the same original name. This was just an oversight and has been fixed, so symbols imported from other chunks should now be renamed when minification is enabled.
-
Provide a friendly error message when you forget
async(#1216)If the parser hits a parse error inside a non-asynchronous function or arrow expression and the previous token is
await, esbuild will now report a friendly error about a missingasynckeyword instead of reporting the parse error. This behavior matches other JavaScript parsers including TypeScript, Babel, and V8.The previous error looked like this:
> test.ts:2:8: error: Expected ";" but found "f" 2 │ await f(); ╵ ^The error now looks like this:
> example.js:2:2: error: "await" can only be used inside an "async" function 2 │ await f(); ╵ ~~~~~ example.js:1:0: note: Consider adding the "async" keyword here 1 │ function f() { │ ^ ╵ async
-
Provide options for how to handle legal comments (#919)
A "legal comment" is considered to be any comment that contains
@licenseor@preserveor that starts with//!or/*!. These comments are preserved in output files by esbuild since that follows the intent of the original authors of the code.However, some people want to remove the automatically-generated license information before they distribute their code. To facilitate this, esbuild now provides several options for how to handle legal comments (via
--legal-comments=in the CLI andlegalCommentsin the JS API):none: Do not preserve any legal commentsinline: Preserve all statement-level legal commentseof: Move all statement-level legal comments to the end of the filelinked: Move all statement-level legal comments to a.LEGAL.txtfile and link to them with a commentexternal: Move all statement-level legal comments to a.LEGAL.txtfile but to not link to them
The default behavior is
eofwhen bundling andinlineotherwise. -
Add
onStartandonEndcallbacks to the plugin APIPlugins can now register callbacks to run when a build is started and ended:
const result = await esbuild.build({ ... incremental: true, plugins: [{ name: 'example', setup(build) { build.onStart(() => console.log('build started')) build.onEnd(result => console.log('build ended', result)) }, }], }) await result.rebuild()One benefit of
onStartandonEndis that they are run for all builds including rebuilds (relevant for incremental mode, watch mode, or serve mode), so they should be a good place to do work related to the build lifecycle.More details:
-
build.onStart()You should not use an
onStartcallback for initialization since it can be run multiple times. If you want to initialize something, just put your plugin initialization code directly inside thesetupfunction instead.The
onStartcallback can beasyncand can return a promise. However, the build does not wait for the promise to be resolved before starting, so a slowonStartcallback will not necessarily slow down the build. AllonStartcallbacks are also run concurrently, not consecutively. The returned promise is purely for error reporting, and matters when theonStartcallback needs to do an asynchronous operation that may fail. If your plugin needs to wait for an asynchronous task inonStartto complete before anyonResolveoronLoadcallbacks are run, you will need to have youronResolveoronLoadcallbacks block on that task fromonStart.Note that
onStartcallbacks do not have the ability to mutatebuild.initialOptions. The initial options can only be modified within thesetupfunction and are consumed once thesetupfunction returns. All rebuilds use the same initial options so the initial options are never re-consumed, and modifications tobuild.initialOptionsthat are done withinonStartare ignored. -
build.onEnd()All
onEndcallbacks are run in serial and each callback is given access to the final build result. It can modify the build result before returning and can delay the end of the build by returning a promise. If you want to be able to inspect the build graph, you should setbuild.initialOptions.metafile = trueand the build graph will be returned as themetafileproperty on the build result object.
-
-
Implement arbitrary module namespace identifiers
This introduces new JavaScript syntax:
import {'🍕' as food} from 'file' export {food as '🧀'}The proposal for this feature appears to not be going through the regular TC39 process. It is being done as a subtle direct pull request instead. It seems appropriate for esbuild to support this feature since it has been implemented in V8 and has now shipped in Chrome 90 and node 16.
According to the proposal, this feature is intended to improve interop with non-JavaScript languages which use exports that aren't valid JavaScript identifiers such as
Foo::~Foo. In particular, WebAssembly allows any valid UTF-8 string as to be used as an export alias.This feature was actually already partially possible in previous versions of JavaScript via the computed property syntax:
import * as ns from './file.json' console.log(ns['🍕'])However, doing this is very un-ergonomic and exporting something as an arbitrary name is impossible outside of
export * from. So this proposal is designed to fully fill out the possibility matrix and make arbitrary alias names a proper first-class feature. -
Implement more accurate
sideEffectsbehavior from Webpack (#1184)This release adds support for the implicit
**/prefix that must be added to paths in thesideEffectsarray inpackage.jsonif the path does not contain/. Another way of saying this is ifpackage.jsoncontains asideEffectsarray with a string that doesn't contain a/then it should be treated as a file name instead of a path. Previously esbuild treated all strings in this array as paths, which does not match how Webpack behaves. The result of this meant that esbuild could consider files to have no side effects while Webpack would consider the same files to have side effects. This bug should now be fixed.
-
Implement ergonomic brand checks for private fields
This introduces new JavaScript syntax:
class Foo { #field static isFoo(x) { return #foo in x // This is an "ergonomic brand check" } } assert(Foo.isFoo(new Foo))The TC39 proposal for this feature is currently at stage 3 but has already been shipped in Chrome 91 and has also landed in Firefox. It seems reasonably inevitable given that it's already shipping and that it's a very simple feature, so it seems appropriate to add this feature to esbuild.
-
Add the
--allow-overwriteflag (#1152)This is a new flag that allows output files to overwrite input files. It's not enabled by default because doing so means overwriting your source code, which can lead to data loss if your code is not checked in. But supporting this makes certain workflows easier by avoiding the need for a temporary directory so doing this is now supported.
-
Minify property accesses on object literals (#1166)
The code
{a: {b: 1}}.a.bwill now be minified to1. This optimization is relatively complex and hard to do safely. Here are some tricky cases that are correctly handled:var obj = {a: 1} assert({a: 1, a: 2}.a === 2) assert({a: 1, [String.fromCharCode(97)]: 2}.a === 2) assert({__proto__: obj}.a === 1) assert({__proto__: null}.a === undefined) assert({__proto__: null}.__proto__ === undefined) assert({a: function() { return this.b }, b: 1}.a() === 1) assert(({a: 1}.a = 2) === 2) assert(++{a: 1}.a === 2) assert.throws(() => { new ({ a() {} }.a) }) -
Improve arrow function parsing edge cases
There are now more situations where arrow expressions are not allowed. This improves esbuild's alignment with the JavaScript specification. Some examples of cases that were previously allowed but that are now no longer allowed:
1 + x => {} console.log(x || async y => {}) class Foo extends async () => {} {}
-
Fix a bug where
-0and0were collapsed to the same value (#1159)Previously esbuild would collapse
Object.is(x ? 0 : -0, -0)intoObject.is((x, 0), -0)during minification, which is incorrect. The IEEE floating-point value-0is a different bit pattern than0and while they both compare equal, the difference is detectable in a few scenarios such as when usingObject.is(). The minification transformation now checks for-0vs.0and no longer has this bug. This fix was contributed by @rtsao. -
Match the TypeScript compiler's output in a strange edge case (#1158)
With this release, esbuild's TypeScript-to-JavaScript transform will no longer omit the namespace in this case:
namespace Something { export declare function Print(a: string): void } Something.Print = function(a) {}This was previously omitted because TypeScript omits empty namespaces, and the namespace was considered empty because the
export declare functionstatement isn't "real":namespace Something { export declare function Print(a: string): void setTimeout(() => Print('test')) } Something.Print = function(a) {}The TypeScript compiler compiles the above code into the following:
var Something; (function (Something) { setTimeout(() => Print('test')); })(Something || (Something = {})); Something.Print = function (a) { };Notice how
Something.Printis never called, and what appears to be a reference to thePrintsymbol on the namespaceSomethingis actually a reference to the global variablePrint. I can only assume this is a bug in TypeScript, but it's important to replicate this behavior inside esbuild for TypeScript compatibility.The TypeScript-to-JavaScript transform in esbuild has been updated to match the TypeScript compiler's output in both of these cases.
-
Separate the
debuglog level intodebugandverboseYou can now use
--log-level=debugto get some additional information that might indicate some problems with your build, but that has a high-enough false-positive rate that it isn't appropriate for warnings, which are on by default. Enabling thedebuglog level no longer generates a torrent of debug information like it did in the past; that behavior is now reserved for theverboselog level instead.
-
Initial support for Deno (#936)
You can now use esbuild in the Deno JavaScript environment via esbuild's official Deno package. Using it looks something like this:
import * as esbuild from 'https://deno.land/x/esbuild@v0.11.11/mod.js' const ts = 'let hasProcess: boolean = typeof process != "null"' const result = await esbuild.transform(ts, { loader: 'ts', logLevel: 'warning' }) console.log('result:', result) esbuild.stop()It has basically the same API as esbuild's npm package with one addition: you need to call
stop()when you're done because unlike node, Deno doesn't provide the necessary APIs to allow Deno to exit while esbuild's internal child process is still running. -
Remove warnings about non-bundled use of
requireandimport(#1153, #1142, #1132, #1045, #812, #661, #574, #512, #495, #480, #453, #410, #80)Previously esbuild had warnings when bundling about uses of
requireandimportthat are not of the formrequire(<string literal>)orimport(<string literal>). These warnings existed because the bundling process must be able to statically-analyze all dynamic imports to determine which files must be included. Here are some real-world examples of cases that esbuild doesn't statically analyze:-
From
mongoose:require('./driver').set(require(global.MONGOOSE_DRIVER_PATH)); -
From
moment:aliasedRequire = require; aliasedRequire('./locale/' + name); -
From
logform:function exposeFormat(name) { Object.defineProperty(format, name, { get() { return require(`./${name}.js`); } }); } exposeFormat('align');
All of these dynamic imports will not be bundled (i.e. they will be left as-is) and will crash at run-time if they are evaluated. Some of these crashes are ok since the code paths may have error handling or the code paths may never be used. Other crashes are not ok because the crash will actually be hit.
The warning from esbuild existed to let you know that esbuild is aware that it's generating a potentially broken bundle. If you discover that your bundle is broken, it's nice to have a warning from esbuild to point out where the problem is. And it was just a warning so the build process still finishes and successfully generates output files. If you didn't want to see the warning, it was easy to turn it off via
--log-level=error.However, there have been quite a few complaints about this warning. Some people seem to not understand the difference between a warning and an error, and think the build has failed even though output files were generated. Other people do not want to see the warning but also do not want to enable
--log-level=error.This release removes this warning for both
requireandimport. Now when you try to bundle code with esbuild that contains dynamic imports not of the formrequire(<string literal>)orimport(<string literal>), esbuild will just silently generate a potentially broken bundle. This may affect people coming from other bundlers that support certain forms of dynamic imports that are not compatible with esbuild such as the Webpack-specific dynamicimport()with pattern matching. -
-
Provide more information about
exportsmap import failures if possible (#1143)Node has a new feature where you can add an
exportsmap to yourpackage.jsonfile to control how external import paths map to the files in your package. You can change which paths map to which files as well as make it impossible to import certain files (i.e. the files are private).If path resolution fails due to an
exportsmap and the failure is not related to import conditions, esbuild's current error message for this just says that the import isn't possible:> example.js:1:15: error: Could not resolve "vanillajs-datepicker/js/i18n/locales/ca" (mark it as external to exclude it from the bundle) 1 │ import ca from 'vanillajs-datepicker/js/i18n/locales/ca' ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ node_modules/vanillajs-datepicker/package.json:6:13: note: The path "./js/i18n/locales/ca" is not exported by package "vanillajs-datepicker" 6 │ "exports": { ╵ ^This error message matches the error that node itself throws. However, the message could be improved in the case where someone is trying to import a file using its file system path and that path is actually exported by the package, just under a different export path. This case comes up a lot when using TypeScript because the TypeScript compiler (and therefore the Visual Studio Code IDE) still doesn't support package
exports.With this release, esbuild will now do a reverse lookup of the file system path using the
exportsmap to determine what the correct import path should be:> example.js:1:15: error: Could not resolve "vanillajs-datepicker/js/i18n/locales/ca" (mark it as external to exclude it from the bundle) 1 │ import ca from 'vanillajs-datepicker/js/i18n/locales/ca' ╵ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ node_modules/vanillajs-datepicker/package.json:6:13: note: The path "./js/i18n/locales/ca" is not exported by package "vanillajs-datepicker" 6 │ "exports": { ╵ ^ node_modules/vanillajs-datepicker/package.json:12:19: note: The file "./js/i18n/locales/ca.js" is exported at path "./locales/ca" 12 │ "./locales/*": "./js/i18n/locales/*.js", ╵ ~~~~~~~~~~~~~~~~~~~~~~~~ example.js:1:15: note: Import from "vanillajs-datepicker/locales/ca" to get the file "node_modules/vanillajs-datepicker/js/i18n/locales/ca.js" 1 │ import ca from 'vanillajs-datepicker/js/i18n/locales/ca' │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ╵ "vanillajs-datepicker/locales/ca"Hopefully this should enable people encountering this issue to fix the problem themselves.
-
Fix escaping of non-BMP characters in property names (#977)
Property names in object literals do not have to be quoted if the property is a valid JavaScript identifier. This is defined as starting with a character in the
ID_StartUnicode category and ending with zero or more characters in theID_ContinueUnicode category. However, esbuild had a bug where non-BMP characters (i.e. characters encoded using two UTF-16 code units instead of one) were always checked againstID_Continueinstead ofID_Startbecause they included a code unit that wasn't at the start. This could result in invalid JavaScript being generated when using--charset=utf8becauseID_Continueis a superset ofID_Startand contains some characters that are not valid at the start of an identifier. This bug has been fixed. -
Be maximally liberal in the interpretation of the
browserfield (#740)The
browserfield inpackage.jsonis an informal convention followed by browser-specific bundlers that allows package authors to substitute certain node-specific import paths with alternative browser-specific import paths. It doesn't have a rigorous specification and the canonical description of the feature doesn't include any tests. As a result, each bundler implements this feature differently. I have tried to create a survey of how different bundlers interpret thebrowserfield and the results are very inconsistent.This release attempts to change esbuild to support the union of the behavior of all other bundlers. That way if people have the
browserfield working with some other bundler and they switch to esbuild, thebrowserfield shouldn't ever suddenly stop working. This seemed like the most principled approach to take in this situation.The drawback of this approach is that it means the
browserfield may start working when switching to esbuild when it was previously not working. This could cause bugs, but I consider this to be a problem with the package (i.e. not using a more well-supported form of thebrowserfield), not a problem with esbuild itself.
-
Fix hash calculation for code splitting and dynamic imports (#1076)
The hash included in the file name of each output file is intended to change if and only if anything relevant to the content of that output file changes. It includes:
- The contents of the file with the paths of other output files omitted
- The output path of the file the final hash omitted
- Some information about the input files involved in that output file
- The contents of the associated source map, if there is one
- All of the information above for all transitive dependencies found by following
importstatements
However, this didn't include dynamic
import()expressions due to an oversight. With this release, dynamicimport()expressions are now also counted as transitive dependencies. This fixes an issue where the content of an output file could change without its hash also changing. As a side effect of this change, dynamic imports inside output files of other output files are now listed in the metadata file if themetafilesetting is enabled. -
Refactor the internal module graph representation
This release changes a large amount of code relating to esbuild's internal module graph. The changes are mostly organizational and help consolidate most of the logic around maintaining various module graph invariants into a separate file where it's easier to audit. The Go language doesn't have great abstraction capabilities (e.g. no zero-cost iterators) so the enforcement of this new abstraction is unfortunately done by convention instead of by the compiler, and there is currently still some code that bypasses the abstraction. But it's better than it was before.
Another relevant change was moving a number of special cases that happened during the tree shaking traversal into the graph itself instead. Previously there were quite a few implicit dependency rules that were checked in specific places, which was hard to follow. Encoding these special case constraints into the graph itself makes the problem easier to reason about and should hopefully make the code more regular and robust.
Finally, this set of changes brings back full support for the
sideEffectsannotation inpackage.json. It was previously disabled when code splitting was active as a temporary measure due to the discovery of some bugs in that scenario. But I believe these bugs have been resolved now that tree shaking and code splitting are done in separate passes (see the previous release for more information).
-
Fix incorrect chunk reference with code splitting, css, and dynamic imports (#1125)
This release fixes a bug where when you use code splitting, CSS imports in JS, and dynamic imports all combined, the dynamic import incorrectly references the sibling CSS chunk for the dynamic import instead of the primary JS chunk. In this scenario the entry point file corresponds to two different output chunks (one for CSS and one for JS) and the wrong chunk was being picked. This bug has been fixed.
-
Split apart tree shaking and code splitting (#1123)
The original code splitting algorithm allowed for files to be split apart and for different parts of the same file to end up in different chunks based on which entry points needed which parts. This was done at the same time as tree shaking by essentially performing tree shaking multiple times, once per entry point, and tracking which entry points each file part is live in. Each file part that is live in at least one entry point was then assigned to a code splitting chunk with all of the other code that is live in the same set of entry points. This ensures that entry points only import code that they will use (i.e. no code will be downloaded by an entry point that is guaranteed to not be used).
This file-splitting feature has been removed because it doesn't work well with the recently-added top-level await JavaScript syntax, which has complex evaluation order rules that operate at file boundaries. File parts now have a single boolean flag for whether they are live or not instead of a set of flags that track which entry points that part is reachable from (reachability is still tracked at the file level).
However, this change appears to have introduced some subtly incorrect behavior with code splitting because there is now an implicit dependency in the import graph between adjacent parts within the same file even if the two parts are unrelated and don't reference each other. This is due to the fact each entry point that references one part pulls in the file (but not the whole file, only the parts that are live in at least one entry point). So liveness must be fully computed first before code splitting is computed.
This release splits apart tree shaking and code splitting into two separate passes, which fixes certain cases where two generated code splitting chunks ended up each importing symbols from the other and causing a cycle. There should hopefully no longer be cycles in generated code splitting chunks.
-
Make
thiswork in static class fields in TypeScript filesCurrently
thisis mis-compiled in static fields in TypeScript files if theuseDefineForClassFieldssetting intsconfig.jsonisfalse(the default value):class Foo { static foo = 123 static bar = this.foo } console.log(Foo.bar)This is currently compiled into the code below, which is incorrect because it changes the value of
this(it's supposed to refer toFoo):class Foo { } Foo.foo = 123; Foo.bar = this.foo; console.log(Foo.bar);This was an intentionally unhandled case because the TypeScript compiler doesn't handle this either (esbuild's currently incorrect output matches the output from the TypeScript compiler, which is also currently incorrect). However, the TypeScript compiler might fix their output at some point in which case esbuild's behavior would become problematic.
So this release now generates the correct output:
const _Foo = class { }; let Foo = _Foo; Foo.foo = 123; Foo.bar = _Foo.foo; console.log(Foo.bar);Presumably the TypeScript compiler will be fixed to also generate something like this in the future. If you're wondering why esbuild generates the extra
_Foovariable, it's defensive code to handle the possibility of the class being reassigned, since class declarations are not constants:class Foo { static foo = 123 static bar = () => Foo.foo } let bar = Foo.bar Foo = { foo: 321 } console.log(bar())We can't just move the initializer containing
Foo.foooutside of the class body because in JavaScript, the class name is shadowed inside the class body by a special hidden constant that is equal to the class object. Even if the class is reassigned later, references to that shadowing symbol within the class body should still refer to the original class object. -
Various fixes for private class members (#1131)
This release fixes multiple issues with esbuild's handling of the
#privatesyntax. Previously there could be scenarios where references tothis.#privatecould be moved outside of the class body, which would cause them to become invalid (since the#privatename is only available within the class body). One such case is when TypeScript'suseDefineForClassFieldssetting has the valuefalse(which is the default value), which causes class field initializers to be replaced with assignment expressions to avoid using "define" semantics:class Foo { static #foo = 123 static bar = Foo.#foo }Previously this was turned into the following code, which is incorrect because
Foo.#foowas moved outside of the class body:class Foo { static #foo = 123; } Foo.bar = Foo.#foo;This is now handled by converting the private field syntax into normal JavaScript that emulates it with a
WeakMapinstead.This conversion is fairly conservative to make sure certain edge cases are covered, so this release may unfortunately convert more private fields than previous releases, even when the target is
esnext. It should be possible to improve this transformation in future releases so that this happens less often while still preserving correctness.
-
Fix an incorrect minification transformation (#1121)
This release removes an incorrect substitution rule in esbuild's peephole optimizer, which is run when minification is enabled. The incorrect rule transformed
if(a && falsy)intoif(a, falsy)which is equivalent iffalsyhas no side effects (such as the literalfalse). However, the rule didn't check that the expression is side-effect free first which could result in miscompiled code. I have removed the rule instead of modifying it to check for the lack of side effects first because while the code is slightly smaller, it may also be more expensive at run-time which is undesirable. The size savings are also very insignificant. -
Change how
NODE_PATHworks to match node (#1117)Node searches for packages in nearby
node_modulesdirectories, but it also allows you to inject extra directories to search for packages in using theNODE_PATHenvironment variable. This is supported when using esbuild's CLI as well as via thenodePathsoption when using esbuild's API.Node's module resolution algorithm is well-documented, and esbuild's path resolution is designed to follow it. The full algorithm is here: https://nodejs.org/api/modules.html#modules_all_together. However, it appears that the documented algorithm is incorrect with regard to
NODE_PATH. The documentation saysNODE_PATHdirectories should take precedence overnode_modulesdirectories, and so that's how esbuild worked. However, in practice node actually does it the other way around.Starting with this release, esbuild will now allow
node_modulesdirectories to take precedence overNODE_PATHdirectories. This is a deviation from the published algorithm. -
Provide a better error message for incorrectly-quoted JSX attributes (#959, #1115)
People sometimes try to use the output of
JSON.stringify()as a JSX attribute when automatically-generating JSX code. Doing so is incorrect because JSX strings work like XML instead of like JS (since JSX is XML-in-JS). Specifically, using a backslash before a quote does not cause it to be escaped:// JSX ends the "content" attribute here and sets "content" to 'some so-called \\' // v let button = <Button content="some so-called \"button text\"" /> // ^ // There is no "=" after the JSX attribute "text", so we expect a ">"It's not just esbuild; Babel and TypeScript also treat this as a syntax error. All of these JSX parsers are just following the JSX specification. This has come up twice now so it could be worth having a dedicated error message. Previously esbuild had a generic syntax error like this:
> example.jsx:1:58: error: Expected ">" but found "\\" 1 │ let button = <Button content="some so-called \"button text\"" /> ╵ ^Now esbuild will provide more information if it detects this case:
> example.jsx:1:58: error: Unexpected backslash in JSX element 1 │ let button = <Button content="some so-called \"button text\"" /> ╵ ^ example.jsx:1:45: note: Quoted JSX attributes use XML-style escapes instead of JavaScript-style escapes 1 │ let button = <Button content="some so-called \"button text\"" /> │ ~~ ╵ " example.jsx:1:29: note: Consider using a JavaScript string inside {...} instead of a quoted JSX attribute 1 │ let button = <Button content="some so-called \"button text\"" /> │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ╵ {"some so-called \"button text\""}
-
Add support for the
overridekeyword in TypeScript 4.3 (#1105)The latest version of TypeScript (now in beta) adds a new keyword called
overridethat can be used on class members. You can read more about this feature in Microsoft's blog post about TypeScript 4.3. It looks like this:class SpecializedComponent extends SomeComponent { override show() { // ... } }With this release, esbuild will now ignore the
overridekeyword when parsing TypeScript code instead of treating this keyword as a syntax error, which means esbuild can now support TypeScript 4.3 syntax. This change was contributed by @g-plane. -
Allow
asyncpluginsetupfunctionsWith this release, you can now return a promise from your plugin's
setupfunction to delay the start of the build:let slowInitPlugin = { name: 'slow-init', async setup(build) { // Delay the start of the build await new Promise(r => setTimeout(r, 1000)) }, }This is useful if your plugin needs to do something asynchronous before the build starts. For example, you may need some asynchronous information before modifying the
initialOptionsobject, which must be done before the build starts for the modifications to take effect. -
Add some optimizations around hashing
This release contains two optimizations to the hashes used in output file names:
-
Hash generation now happens in parallel with other work, and other work only blocks on the hash computation if the hash ends up being needed (which is only if
[hash]is included in--entry-names=, and potentially--chunk-names=if it's relevant). This is a performance improvement because--entry-names=does not include[hash]in the default case, so bundling time no longer always includes hashing time. -
The hashing algorithm has been changed from SHA1 to xxHash (specifically this Go implementation) which means the hashing step is around 6x faster than before. Thanks to @Jarred-Sumner for the suggestion.
-
-
Disable tree shaking annotations when code splitting is active (#1070, #1081)
Support for Webpack's
"sideEffects": falseannotation inpackage.jsonis now disabled when code splitting is enabled and there is more than one entry point. This avoids a bug that could cause generated chunks to reference each other in some cases. Now all chunks generated by code splitting should be acyclic.
-
Avoid name collisions with TypeScript helper functions (#1102)
Helper functions are sometimes used when transforming newer JavaScript syntax for older browsers. For example,
let {x, ...y} = {z}is transformed intolet _a = {z}, {x} = _a, y = __rest(_a, ["x"])which uses the__resthelper function. Many of esbuild's transforms were modeled after the transforms in the TypeScript compiler, so many of the helper functions use the same names as TypeScript's helper functions.However, the TypeScript compiler doesn't avoid name collisions with existing identifiers in the transformed code. This means that post-processing esbuild's output with the TypeScript compiler (e.g. for lowering ES6 to ES5) will cause issues since TypeScript will fail to call its own helper functions: microsoft/TypeScript#43296. There is also a problem where TypeScript's
tsliblibrary overwrites globals with these names, which can overwrite esbuild's helper functions if code bundled with esbuild is run in the global scope.To avoid these problems, esbuild will now use different names for its helper functions.
-
Fix a chunk hashing issue (#1099)
Previously the chunk hashing algorithm skipped hashing entry point chunks when the
--entry-names=setting doesn't contain[hash], since the hash wasn't used in the file name. However, this is no longer correct with the change in version 0.11.0 that made dynamic entry point chunks use--chunk-names=instead of--entry-names=since--chunk-names=can still contain[hash].With this release, chunk contents will now always be hashed regardless of the chunk type. This makes esbuild somewhat slower than before in the common case, but it fixes this correctness issue.
-
Auto-define
process.env.NODE_ENVwhen platform is set tobrowserAll code in the React world has the requirement that the specific expression
process.env.NODE_ENVmust be replaced with a string at compile-time or your code will immediately crash at run-time. This is a common stumbling point for people when they start using esbuild with React. Previously bundling code with esbuild containingprocess.env.NODE_ENVwithout defining a string replacement first was a warning that warned you about the lack of a define.With this release esbuild will now attempt to define
process.env.NODE_ENVautomatically instead of warning about it. This will be implicitly defined to"production"if minification is enabled and"development"otherwise. This automatic behavior only happens when the platform isbrowser, sinceprocessis not a valid browser API and will never exist in the browser. This is also only done if there are no existing defines forprocess,process.env, orprocess.env.NODE_ENVso you can override the automatic value if necessary. If you need to disable this behavior, you can use theneutralplatform instead of thebrowserplatform. -
Retain side-effect free intermediate re-exporting files (#1088)
This fixes a subtle bug with esbuild's support for Webpack's
"sideEffects": falseannotation inpackage.jsonwhen combined with re-export statements. A re-export is when you import something from one file and then export it again. You can re-export something withexport * fromorexport {foo} fromorimport {foo} fromfollowed byexport {foo}.The bug was that files which only contain re-exports and that are marked as being side-effect free were not being included in the bundle if you import one of the re-exported symbols. This is because esbuild's implementation of re-export linking caused the original importing file to "short circuit" the re-export and just import straight from the file containing the final symbol, skipping the file containing the re-export entirely.
This was normally not observable since the intermediate file consisted entirely of re-exports, which have no side effects. However, a recent change to allow ESM files to be lazily-initialized relies on all intermediate files being included in the bundle to trigger the initialization of the lazy evaluation wrappers. So the behavior of skipping over re-export files is now causing the imported symbols to not be initialized if the re-exported file is marked as lazily-evaluated.
The fix is to track all re-exports in the import chain from the original file to the file containing the final symbol and then retain all of those statements if the import ends up being used.
-
Add a very verbose
debuglog levelThis log level is an experiment. Enabling it logs a lot of information (currently only about path resolution). The idea is that if you are having an obscure issue, the debug log level might contain some useful information. Unlike normal logs which are meant to mainly provide actionable information, these debug logs are intentionally mostly noise and are designed to be searched through instead.
Here is an example of debug-level log output:
> debug: Resolving import "react" in directory "src" of type "import-statement" note: Read 26 entries for directory "src" note: Searching for "react" in "node_modules" directories starting from "src" note: Attempting to load "src/react" as a file note: Failed to find file "src/react" note: Failed to find file "src/react.tsx" note: Failed to find file "src/react.ts" note: Failed to find file "src/react.js" note: Failed to find file "src/react.css" note: Failed to find file "src/react.svg" note: Attempting to load "src/react" as a directory note: Failed to read directory "src/react" note: Parsed package name "react" and package subpath "." note: Checking for a package in the directory "node_modules/react" note: Read 7 entries for directory "node_modules/react" note: Read 393 entries for directory "node_modules" note: Attempting to load "node_modules/react" as a file note: Failed to find file "node_modules/react" note: Failed to find file "node_modules/react.tsx" note: Failed to find file "node_modules/react.ts" note: Failed to find file "node_modules/react.js" note: Failed to find file "node_modules/react.css" note: Failed to find file "node_modules/react.svg" note: Attempting to load "node_modules/react" as a directory note: Read 7 entries for directory "node_modules/react" note: Resolved to "node_modules/react/index.js" using the "main" field in "node_modules/react/package.json" note: Read 7 entries for directory "node_modules/react" note: Read 7 entries for directory "node_modules/react" note: Primary path is "node_modules/react/index.js" in namespace "file"
-
Fix missing symbol dependency for wrapped ESM files (#1086)
An internal graph node was missing an edge, which could result in generating code that crashes at run-time when code splitting is enabled. Specifically a part containing an import statement must depend on the imported file's wrapper symbol if the imported file is wrapped, regardless of whether it's a wrapped CommonJS or ESM file. Previously this was only the case for CommonJS files but not for ESM files, which is incorrect. This bug has been fixed.
-
Fix an edge case with entry points and top-level await
If an entry point uses
import()on itself, it currently has to be wrapped sinceimport()expressions call the wrapper for the imported file. This means the another call to the wrapper must be inserted at the bottom of the entry point file to start the lazy evaluation of the entry point code (otherwise nothing will be evaluated, since the entry point is wrapped). However, if this entry point then contains a top-level await that means the wrapper isasyncand must be passed toawaitto catch and forward any exceptions thrown during the evaluation of the entry point code. Thisawaitwas previously missing in this specific case due to a bug, but theawaitshould now be added in this release.
-
Fix a missing space before internal
import()when minifying (#1082)Internal
import()of a CommonJS module inside the bundle turns into a call toPromise.resolve().then(() => require()). However, a space was not inserted before thePromisetoken when minifying, which could lead to a syntax error. This bug has been fixed. -
Fix code generation for unused imported files without side effects (#1080)
When esbuild adds a wrapping closure around a file to turn it from a statically-initialized file to a dynamically-initialized file, it also needs to turn import statements in other files that import the wrapped file into calls to the wrapper so that the wrapped file is initialized in the correct ordering. However, although tree-shaking is disabled for wrapped CommonJS files because CommonJS exports are dynamic, tree-shaking is still enabled for wrapped ESM files because ESM exports are static.
This caused a bug when files that have been marked with
"sideEffects": falseend up being completely unused in the resulting bundle. In that case the file is removed entirely, but esbuild was still turningimportstatements to that file into calls to the ESM wrapper. These wrapper calls should instead be omitted if the file was completely removed from the bundle as dead code. This bug has been fixed. -
Allow top-level await in supported environments
Top-level await (i.e. using the
awaitkeyword outside of anasyncfunction) is not yet part of the JavaScript language standard. The feature proposal is still at stage 3 and has not yet advanced to stage 4. However, V8 has already implemented it and it has shipped in Chrome 89 and node 14.8. This release allows top-level await to be used when the--target=flag is set to those compilation targets. -
Convert
import()torequire()ifimport()is not supported (#1084)This release now converts dynamic
import()expressions intoPromise.resolve().then(() => require())expressions if the compilation target doesn't support them. This is the case for node before version 13.2, for example.
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.10.0. See the documentation about semver for more information.
The changes in this release mostly relate to how entry points are handled. The way output paths are generated has changed in some cases, so you may need to update how you refer to the output path for a given entry point when you update to this release (see below for details). These breaking changes are as follows:
-
Change how
require()andimport()of ESM works (#667, #706)Previously if you call
require()on an ESM file, or callimport()on an ESM file with code splitting disabled, esbuild would convert the ESM file to CommonJS. For example, if you had the following input files:// cjs-file.js console.log(require('./esm-file.js').foo) // esm-file.js export let foo = bar()The previous bundling behavior would generate something like this:
var require_esm_file = __commonJS((exports) => { __markAsModule(exports); __export(exports, { foo: () => foo }); var foo = bar(); }); console.log(require_esm_file().foo);This behavior has been changed and esbuild now generates something like this instead:
var esm_file_exports = {}; __export(esm_file_exports, { foo: () => foo }); var foo; var init_esm_file = __esm(() => { foo = bar(); }); console.log((init_esm_file(), esm_file_exports).foo);The variables have been pulled out of the lazily-initialized closure and are accessible to the rest of the module's scope. Some benefits of this approach:
-
If another file does
import {foo} from "./esm-file.js", it will just referencefoodirectly and will not pay the performance penalty or code size overhead of the dynamic property accesses that come with CommonJS-style exports. So this improves performance and reduces code size in some cases. -
This fixes a long-standing bug (#706) where entry point exports could be broken if the entry point is a target of a
require()call and the output format was ESM. This happened because previously callingrequire()on an entry point converted it to CommonJS, which then meant it only had a singledefaultexport, and the exported variables were inside the CommonJS closure and inaccessible to an ESM-styleexport {}clause. Now callingrequire()on an entry point only causes it to be lazily-initialized but all exports are still in the module scope and can still be exported using a normalexport {}clause. -
Now that this has been changed,
import()of a module with top-level await (#253) is now allowed when code splitting is disabled. Previously this didn't work becauseimport()with code splitting disabled was implemented by converting the module to CommonJS and usingPromise.resolve().then(() => require()), but converting a module with top-level await to CommonJS is impossible because the CommonJS call signature must be synchronous. Now that this implemented using lazy initialization instead of CommonJS conversion, the closure wrapping the ESM file can now beasyncand theimport()expression can be replaced by a call to the lazy initializer. -
Adding the ability for ESM files to be lazily-initialized is an important step toward additional future code splitting improvements including: manual chunk names (#207), correct import evaluation order (#399), and correct top-level await evaluation order (#253). These features all need to make use of deferred evaluation of ESM code.
In addition, calling
require()on an ESM file now recursively wraps all transitive dependencies of that file instead of just wrapping that ESM file itself. This is an increase in the size of the generated code, but it is important for correctness (#667). Callingrequire()on a module means its evaluation order is determined at run-time, which means the evaluation order of all dependencies must also be determined at run-time. If you don't want the increase in code size, you should use animportstatement instead of arequire()call. -
-
Dynamic imports now use chunk names instead of entry names (#1056)
Previously the output paths of dynamic imports (files imported using the
import()syntax) were determined by the--entry-names=setting. However, this can cause problems if you configure the--entry-names=setting to omit both[dir]and[hash]because then two dynamic imports with the same name will cause an output file name collision.Now dynamic imports use the
--chunk-names=setting instead, which is used for automatically-generated chunks. This setting is effectively required to include[hash]so dynamic import name collisions should now be avoided.In addition, dynamic imports no longer affect the automatically-computed default value of
outbase. By defaultoutbaseis computed to be the lowest common ancestor directory of all entry points. Previously dynamic imports were considered entry points in this calculation so adding a dynamic entry point could unexpectedly affect entry point output file paths. This issue has now been fixed. -
Allow custom output paths for individual entry points
By default, esbuild will automatically generate an output path for each entry point by computing the relative path from the
outbasedirectory to the entry point path, and then joining that relative path to theoutdirdirectory. The output path can be customized usingoutpath, but that only works for a single file. Sometimes you may need custom output paths while using multiple entry points. You can now do this by passing the entry points as a map instead of an array:-
CLI
esbuild out1=in1.js out2=in2.js --outdir=out -
JS
esbuild.build({ entryPoints: { out1: 'in1.js', out2: 'in2.js', }, outdir: 'out', }) -
Go
api.Build(api.BuildOptions{ EntryPointsAdvanced: []api.EntryPoint{{ OutputPath: "out1", InputPath: "in1.js", }, { OutputPath: "out2", InputPath: "in2.js", }}, Outdir: "out", })
This will cause esbuild to generate the files
out/out1.jsandout/out2.jsinside the output directory. These custom output paths are used as input for the--entry-names=path template setting, so you can use something like--entry-names=[dir]/[name]-[hash]to add an automatically-computed hash to each entry point while still using the custom output path. -
-
Derive entry point output paths from the original input path (#945)
Previously esbuild would determine the output path for an entry point by looking at the post-resolved path. For example, running
esbuild --bundle react --outdir=outwould generate the output pathout/index.jsbecause the input pathreactwas resolved tonode_modules/react/index.js. With this release, the output path is now determined by looking at the pre-resolved path. For example, runningesbuild --bundle react --outdir=outnow generates the output pathout/react.js. If you need to keep using the output path that esbuild previously generated with the old behavior, you can use the custom output path feature (described above). -
Use the
filenamespace for file entry points (#791)Plugins that contain an
onResolvecallback with thefilefilter don't apply to entry point paths because it's not clear that entry point paths are files. For example, you could potentially bundle an entry point ofhttps://www.example.com/file.jswith a HTTP plugin that automatically downloads data from the server at that URL. But this behavior can be unexpected for people writing plugins.With this release, esbuild will do a quick check first to see if the entry point path exists on the file system before running plugins. If it exists as a file, the namespace will now be
filefor that entry point path. This only checks the exact entry point name and doesn't attempt to search for the file, so for example it won't handle cases where you pass a package path as an entry point or where you pass an entry point without an extension. Hopefully this should help improve this situation in the common case where the entry point is an exact path.
In addition to the breaking changes above, the following features are also included in this release:
-
Warn about mutation of private methods (#1067)
Mutating a private method in JavaScript is not allowed, and will throw at run-time:
class Foo { #method() {} mutate() { this.#method = () => {} } }This is the case both when esbuild passes the syntax through untransformed and when esbuild transforms the syntax into the equivalent code that uses a
WeakSetto emulate private methods in older browsers. However, it's clear from this code that doing this will always throw, so this code is almost surely a mistake. With this release, esbuild will now warn when you do this. This change was contributed by @jridgewell. -
Fix some obscure TypeScript type parsing edge cases
In TypeScript, type parameters come after a type and are placed in angle brackets like
Foo<T>. However, certain built-in types do not accept type parameters including primitive types such asnumber. This meansif (x as number < 1) {}is not a syntax error whileif (x as Foo < 1) {}is a syntax error. This release changes TypeScript type parsing to allow type parameters in a more restricted set of situations, which should hopefully better resolve these type parsing ambiguities.
-
Fix a crash that was introduced in the previous release (#1064)
This crash happens when code splitting is active and there is a CSS entry point as well as two or more JavaScript entry points. There is a known issue where CSS bundling does not work when code splitting is active (code splitting is still a work in progress, see #608) so doing this will likely not work as expected. But esbuild obviously shouldn't crash. This release fixes the crash, although esbuild still does not yet generate the correct CSS output in this case.
-
Fix private fields inside destructuring assignment (#1066)
Private field syntax (i.e.
this.#field) is supported for older language targets by converting the code into accesses into aWeakMap. However, although regular assignment (i.e.this.#field = 1) was handled destructuring assignment (i.e.[this.#field] = [1]) was not handled due to an oversight. Support for private fields inside destructuring assignment is now included with this release. -
Fix an issue with direct
evaland top-level symbolsIt was previously the case that using direct
evalcaused the file containing it to be considered a CommonJS file, even if the file used ESM syntax. This was because the evaluated code could potentially attempt to interact with top-level symbols by name and the CommonJS closure was used to isolate those symbols from other modules so their names could be preserved (otherwise their names may need to be renamed to avoid collisions). However, ESM files are no longer convertable to CommonJS files due to the need to support top-level await.This caused a bug where scope hoisting could potentially merge two modules containing direct
evaland containing the same top-level symbol name into the same scope. These symbols were prevented from being renamed due to the directeval, which caused a syntax error at run-time due to the name collision.Because of this, esbuild is dropping the guarantee that using direct
evalin an ESM file will be able to access top-level symbols. These symbols are now free to be renamed to avoid name collisions, and will now be minified when identifier minification is enabled. This is unlikely to affect real-world code because most real-world uses of directevalonly attempt to access local variables, not top-level symbols.Using direct
evalin an ESM file when bundling with esbuild will generate a warning. The warning is not new and is present in previous releases of esbuild as well. The way to avoid the warning is to avoid directeval, since directevalis somewhat of an anti-pattern and there are better alternatives.
-
Expose
metafiletoonRebuildin watch mode (#1057)Previously the build results returned to the watch mode
onRebuildcallback was missing themetafileproperty when themetafile: trueoption was present. This bug has been fixed. -
Add a
formatMessagesAPI (#1058)This API lets you print log messages to the terminal using the same log format that esbuild itself uses. This can be used to filter esbuild's warnings while still making the output look the same. Here's an example of calling this API:
import esbuild from 'esbuild' const formatted = await esbuild.formatMessages([{ text: '"test" has already been declared', location: { file: 'file.js', line: 2, column: 4, length: 4, lineText: 'let test = "second"' }, notes: [{ text: '"test" was originally declared here', location: { file: 'file.js', line: 1, column: 4, length: 4, lineText: 'let test = "first"' }, }], }], { kind: 'error', color: true, terminalWidth: 100, }) process.stdout.write(formatted.join('')) -
Remove the file splitting optimization (#998)
This release removes the "file splitting optimization" that has up to this point been a part of esbuild's code splitting algorithm. This optimization allowed code within a single file to end up in separate chunks as long as that code had no side effects. For example, bundling two entry points that both use a disjoint set of code from a shared file consisting only of code without side effects would previously not generate any shared code chunks at all.
This optimization is being removed because the top-level await feature was added to JavaScript after this optimization was added, and performing this optimization in the presence of top-level await is more difficult than before. The correct evaulation order of a module graph containing top-level await is extremely complicated and is specified at the module boundary. Moving code that is marked as having no side effects across module boundaries under these additional constraints is even more complexity and is getting in the way of implementing top-level await. So the optimization has been removed to unblock work on top-level await, which esbuild must support.
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.9.0. See the documentation about semver for more information.
That said, there are no breaking API changes in this release. The breaking changes are instead about how input files are interpreted and/or how output files are generated in some cases. So upgrading should be relatively straightforward as your API calls should still work the same way, but please make sure to test your code when you upgrade because the output may be different. These breaking changes are as follows:
-
No longer support
moduleorexportsin an ESM file (#769)This removes support for using CommonJS exports in a file with ESM exports. Previously this worked by converting the ESM file to CommonJS and then mixing the CommonJS and ESM exports into the same
exportsobject. But it turns out that supporting this is additional complexity for the bundler, so it has been removed. It's also not something that works in real JavaScript environments since modules will never support both export syntaxes at once.Note that this doesn't remove support for using
requirein ESM files. Doing this still works (and can be made to work in a real ESM environment by assigning toglobalThis.require). This also doesn't remove support for usingimportin CommonJS files. Doing this also still works. -
No longer change
import()torequire()(#1029)Previously esbuild's transform for
import()matched TypeScript's behavior, which is to transform it intoPromise.resolve().then(() => require())when the current output format is something other than ESM. This was done when an import is external (i.e. not bundled), either due to the expression not being a string or due to the string matching an external import path.With this release, esbuild will no longer do this. Now
import()expressions will be preserved in the output instead. These expressions can be handled in non-ESM code by arranging for theimportidentifier to be a function that imports ESM code. This is how node works, so it will now be possible to useimport()with node when the output format is something other than ESM. -
Run-time
export * asstatements no longer convert the file to CommonJSCertain
export * asstatements require a bundler to evaluate them at run-time instead of at compile-time like the JavaScript specification. This is the case when re-exporting symbols from an external file and a file in CommonJS format.Previously esbuild would handle this by converting the module containing the
export * asstatement to CommonJS too, since CommonJS exports are evaluated at run-time while ESM exports are evaluated at bundle-time. However, this is undesirable because tree shaking only works for ESM, not for CommonJS, and the CommonJS wrapper causes additional code bloat. Another upcoming problem is that top-level await cannot work within a CommonJS module because CommonJSrequire()is synchronous.With this release, esbuild will now convert modules containing a run-time
export * asstatement to a special ESM-plus-dynamic-fallback mode. In this mode, named exports present at bundle time can still be imported directly by name, but any imports that don't match one of the explicit named imports present at bundle time will be converted to a property access on the fallback object instead of being a bundle error. These property accesses are then resolved at run-time and will be undefined if the export is missing. -
Change whether certain files are interpreted as ESM or CommonJS (#1043)
The bundling algorithm currently doesn't contain any logic that requires flagging modules as CommonJS vs. ESM beforehand. Instead it handles a superset and then sort of decides later if the module should be treated as CommonJS vs. ESM based on whether the module uses the
moduleorexportsvariables and/or theexportskeyword.With this release, files that follow node's rules for module types will be flagged as explicitly ESM. This includes files that end in
.mjsand files within a package containing"type": "module"in the enclosingpackage.jsonfile. The CommonJSmoduleandexportsfeatures will be unavailable in these files. This matters most for files without any exports, since then it's otherwise ambiguous what the module type is.In addition, files without exports should now accurately fall back to being considered CommonJS. They should now generate a
defaultexport of an empty object when imported using animportstatement, since that's what happens in node when you import a CommonJS file into an ESM file in node. Previously the default export could be undefined because these export-less files were sort of treated as ESM but with missing import errors turned into warnings instead.This is an edge case that rarely comes up in practice, since you usually never import things from a module that has no exports.
In addition to the breaking changes above, the following features are also included in this release:
-
Initial support for bundling with top-level await (#253)
Top-level await is a feature that lets you use an
awaitexpression at the top level (outside of anasyncfunction). Here is an example:let promise = fetch('https://www.example.com/data') export let data = await promise.then(x => x.json())Top-level await only works in ECMAScript modules, and does not work in CommonJS modules. This means that you must use an
importstatement or animport()expression to import a module containing top-level await. You cannot userequire()because it's synchronous while top-level await is asynchronous. There should be a descriptive error message when you try to do this.This initial release only has limited support for top-level await. It is only supported with the
esmoutput format, but not with theiifeorcjsoutput formats. In addition, the compilation is not correct in that two modules that both contain top-level await and that are siblings in the import graph will be evaluated in serial instead of in parallel. Full support for top-level await will come in a future release. -
Add the ability to set
sourceRootin source maps (#1028)You can now use the
--source-root=flag to set thesourceRootfield in source maps generated by esbuild. When asourceRootis present in a source map, all source paths are resolved relative to it. This is particularly useful when you are hosting compiled code on a server and you want to point the source files to a GitHub repo, such as what AMP does.Here is the description of
sourceRootfrom the source map specification:An optional source root, useful for relocating source files on a server or removing repeated values in the "sources" entry. This value is prepended to the individual entries in the "source" field. If the sources are not absolute URLs after prepending of the "sourceRoot", the sources are resolved relative to the SourceMap (like resolving script src in a html document).
This feature was contributed by @jridgewell.
-
Allow plugins to return custom file watcher paths
Currently esbuild's watch mode automatically watches all file system paths that are handled by esbuild itself, and also automatically watches the paths of files loaded by plugins when the paths are in the
filenamespace. The paths of files that plugins load in namespaces other than thefilenamespace are not automatically watched.Also, esbuild never automatically watches any file system paths that are consulted by the plugin during its processing, since esbuild is not aware of those paths. For example, this means that if a plugin calls
require.resolve(), all of the various "does this file exist" checks that it does will not be watched automatically. So if one of those files is created in the future, esbuild's watch mode will not rebuild automatically even though the build is now outdated.To fix this problem, this release introduces the
watchFilesandwatchDirsproperties on plugin return values. Plugins can specify these to add additional custom file system paths to esbuild's internal watch list. Paths in thewatchFilesarray cause esbuild to rebuild if the file contents change, and paths in thewatchDirsarray cause esbuild to rebuild if the set of directory entry names changes for that directory path.Note that
watchDirsdoes not cause esbuild to rebuild if any of the contents of files inside that directory are changed. It also does not recursively traverse through subdirectories. It only watches the set of directory entry names (i.e. the output of the Unixlscommand).
-
Add support for Android on ARM 64-bit (#803)
This release includes support for Android in the official
esbuildpackage. It should now be possible to install and run esbuild on Android devices through npm. -
Fix incorrect MIME types on Windows (#1030)
The web server built into esbuild uses the file extension to determine the value of the
Content-Typeheader. This was previously done using themime.TypeByExtension()function from Go's standard library. However, this function is apparently broken on Windows because installed programs can change MIME types in the Windows registry: golang/go#32350. This release fixes the problem by using a copy of Go'smime.TypeByExtension()function without the part that reads from the Windows registry. -
Using a top-level return inside an ECMAScript module is now forbidden
The CommonJS module format is implemented as an anonymous function wrapper, so technically you can use a top-level
returnstatement and it will actually work. Some packages in the wild use this to exit early from module initialization, so esbuild supports this. However, the ECMAScript module format doesn't allow top-level returns. With this release, esbuild no longer allows top-level returns in ECMAScript modules.
-
Expose build options to plugins (#373)
Plugins can now access build options from within the plugin using the
initialOptionsproperty. For example:let nodeEnvPlugin = { name: 'node-env', setup(build) { const options = build.initialOptions options.define = options.define || {} options.define['process.env.NODE_ENV'] = options.minify ? '"production"' : '"development"' }, } -
Fix an edge case with the object spread transform (#1017)
This release fixes esbuild's object spread transform in cases where property assignment could be different than property definition. For example:
console.log({ get x() {}, ...{x: 1}, })This should print
{x: 1}but transforming this through esbuild with--target=es6causes the resulting code to throw an error. The problem is that esbuild currently transforms this code to a call toObject.assignand that uses property assignment semantics, which causes the assignment to throw (since you can't assign to a getter-only property).With this release, esbuild will now transform this into code that manually loops over the properties and copies them over one-by-one using
Object.definePropertyinstead. This uses property definition semantics which better matches the specification. -
Fix a TypeScript parsing edge case with arrow function return types (#1016)
This release fixes the following TypeScript parsing edge case:
():Array<number>=>{return [1]}This was tripping up esbuild's TypeScript parser because the
>=token was split into a>token and a=token because the>token is needed to close the type parameter list, but the=token was not being combined with the following>token to form a=>token. This is normally not an issue because there is normally a space in between the>and the=>tokens here. The issue only happened when the spaces were removed. This bug has been fixed. Now after the>=token is split, esbuild will expand the=token into the following characters if possible, which can result in a=>,==, or===token. -
Enable faster synchronous transforms under a flag (#1000)
Currently the synchronous JavaScript API calls
transformSyncandbuildSyncspawn a new child process on every call. This is due to limitations with node'schild_processAPI. Doing this meanstransformSyncandbuildSyncare much slower thantransformandbuild, which share the same child process across calls.There was previously a workaround for this limitation that uses node's
worker_threadsAPI and atomics to block the main thread while asynchronous communication happens in a worker, but that was reverted due to a bug in node'sworker_threadsimplementation. Now that this bug has been fixed by node, I am re-enabling this workaround. This should result intransformSyncandbuildSyncbeing much faster.This approach is experimental and is currently only enabled if the
ESBUILD_WORKER_THREADSenvironment variable is present. If this use case matters to you, please try it out and let me know if you find any problems with it. -
Update how optional chains are compiled to match new V8 versions (#1019)
An optional chain is an expression that uses the
?.operator, which roughly avoids evaluation of the right-hand side if the left-hand side isnullorundefined. Soa?.bis basically equivalent toa == null ? void 0 : a.b. When the language target is set toes2019or below, esbuild will transform optional chain expressions into equivalent expressions that do not use the?.operator.This transform is designed to match the behavior of V8 exactly, and is designed to do something similar to the equivalent transform done by the TypeScript compiler. However, V8 has recently changed its behavior in two cases:
-
Forced call of an optional member expression should propagate the object to the method:
const o = { m() { return this; } }; assert((o?.m)() === o);V8 bug: https://bugs.chromium.org/p/v8/issues/detail?id=10024
-
Optional call of
evalmust be an indirect eval:globalThis.a = 'global'; var b = (a => eval?.('a'))('local'); assert(b === 'global');V8 bug: https://bugs.chromium.org/p/v8/issues/detail?id=10630
This release changes esbuild's transform to match V8's new behavior. The transform in the TypeScript compiler is still emulating the old behavior as of version 4.2.3, so these syntax forms should be avoided in TypeScript code for portability.
-
-
Fix parsing of the
[dir]placeholder (#1013)The entry names feature in the previous release accidentally didn't include parsing for the
[dir]placeholder, so the[dir]placeholder was passed through verbatim into the resulting output paths. This release fixes the bug, which means you can now use the[dir]placeholder. Sorry about the oversight.
-
Enable hashes in entry point file paths (#518)
This release adds the new
--entry-names=flag. It's similar to the--chunk-names=and--asset-names=flags except it sets the output paths for entry point files. The pattern defaults to[dir]/[name]which should be equivalent to the previous entry point output path behavior, so this should be a backward-compatible change.This change has the following consequences:
-
It is now possible for entry point output paths to contain a hash. For example, this now happens if you pass
--entry-names=[dir]/[name]-[hash]. This means you can now use esbuild to generate output files such that all output paths have a hash in them, which means it should now be possible to serve the output files with an infinite cache lifetime so they are only downloaded once and then cached by the browser forever. -
It is now possible to prevent the generation of subdirectories inside the output directory. Previously esbuild replicated the directory structure of the input entry points relative to the
outbasedirectory (which defaults to the lowest common ancestor directory across all entry points). This value is substituted into the newly-added[dir]placeholder. But you can now omit it by omitting that placeholder, like this:--entry-names=[name]. -
Source map names should now be equal to the corresponding output file name plus an additional
.mapextension. Previously the hashes were content hashes, so the source map had a different hash than the corresponding output file because they had different contents. Now they have the same hash so finding the source map should now be easier (just add.map). -
Due to the way the new hashing algorithm works, all chunks can now be generated fully in parallel instead of some chunks having to wait until their dependency chunks have been generated first. The import paths for dependency chunks are now swapped in after chunk generation in a second pass (detailed below). This could theoretically result in a speedup although I haven't done any benchmarks around this.
Implementing this feature required overhauling how hashes are calculated to prevent the chicken-and-egg hashing problem due to dynamic imports, which can cause cycles in the import graph of the resulting output files when code splitting is enabled. Since generating a hash involved first hashing all of your dependencies, you could end up in a situation where you needed to know the hash to calculate the hash (if a file was a dependency of itself).
The hashing algorithm now works in three steps (potentially subject to change in the future):
-
The initial versions of all output files are generated in parallel, with temporary paths used for any imports of other output files. Each temporary path is a randomly-generated string that is unique for each output file. An initial source map is also generated at this step if source maps are enabled.
The hash for the first step includes: the raw content of the output file excluding the temporary paths, the relative file paths of all input files present in that output file, the relative output path for the resulting output file (with
[hash]for the hash that hasn't been computed yet), and contents of the initial source map. -
After the initial versions of all output files have been generated, calculate the final hash and final output path for each output file. Calculating the final output path involves substituting the final hash for the
[hash]placeholder in the entry name template.The hash for the second step includes: the hash from the first step for this file and all of its transitive dependencies.
-
After all output files have a final output path, the import paths in each output file for importing other output files are substituted. Source map offsets also have to be adjusted because the final output path is likely a different length than the temporary path used in the first step. This is also done in parallel for each output file.
This whole algorithm roughly means the hash of a given output file should change if an only if any input file in that output file or any output file it depends on is changed. So the output path and therefore the browser's cache key should not change for a given output file in between builds if none of the relevant input files were changed.
-
-
Fix importing a path containing a
?character on Windows (#989)On Windows, the
?character is not allowed in path names. This causes esbuild to fail to import paths containing this character. This is usually fine because people don't put?in their file names for this reason. However, the import paths for some ancient CSS code contains the?character as a hack to work around a bug in Internet Explorer:@font-face { src: url("./icons.eot?#iefix") format('embedded-opentype'), url("./icons.woff2") format('woff2'), url("./icons.woff") format('woff'), url("./icons.ttf") format('truetype'), url("./icons.svg#icons") format('svg'); }The intent is for the bundler to ignore the
?#iefixpart. However, there may actually be a file calledicons.eot?#iefixon the file system so esbuild checks the file system for bothicons.eot?#iefixandicons.eot. This check was triggering this issue. With this release, an invalid path is considered the same as a missing file so bundling code like this should now work on Windows. -
Parse and ignore the deprecated
@-ms-viewportCSS rule (#997)The
@viewportrule has been deprecated and removed from the web. Modern browsers now completely ignore this rule. However, in theory it sounds like would still work for mobile versions of Internet Explorer, if those still exist. The https://ant.design/ library contains an instance of the@-ms-viewportrule and it currently causes a warning with esbuild, so this release adds support for parsing this rule to disable the warning. -
Avoid mutating the binary executable file in place (#963)
This release changes the install script for the
esbuildnpm package to use the "rename a temporary file" approach instead of the "write the file directly" approach to replace theesbuildcommand stub file with the real binary executable. This should hopefully work around a problem with the pnpm package manager and its use of hard links. -
Avoid warning about potential issues with
sideEffectsin packages (#999)Bare imports such as
import "foo"mean the package is only imported for its side effects. Doing this when the package contains"sideEffects": falseinpackage.jsoncauses a warning because it means esbuild will not import the file since it has been marked as having no side effects, even though the import statement clearly expects it to have side effects. This is usually caused by an incorrectsideEffectsannotation in the package.However, this warning is not immediately actionable if the file containing the import statement is itself in a package. So with this release, esbuild will no longer issue this warning if the file containing the import is inside a
node_modulesfolder. Note that even though the warning is no longer there, this situation can still result in a broken bundle if thesideEffectsannotation is incorrect.
-
Fix path resolution with the
exportsfield for scoped packagesThis release fixes a bug where the
exportsfield inpackage.jsonfiles was not being detected for scoped packages (i.e. packages of the form@scope/pkg-nameinstead of justpkg-name). Theexportsfield should now be respected for these kinds of packages. -
Improved error message in
exportsfailure caseNode's new conditional exports feature can be non-intuitive and hard to use. Now that esbuild supports this feature (as of version 0.9.0), you can get into a situation where it's impossible to import a package if the package's
exportsfield in itspackage.jsonfile isn't configured correctly.Previously the error message for this looked like this:
> entry.js:1:7: error: Could not resolve "jotai" (mark it as external to exclude it from the bundle) 1 │ import 'jotai' ╵ ~~~~~~~ node_modules/jotai/package.json:16:13: note: The path "." is not exported by "jotai" 16 │ "exports": { ╵ ^With this release, the error message will now provide additional information about why the package cannot be imported:
> entry.js:1:7: error: Could not resolve "jotai" (mark it as external to exclude it from the bundle) 1 │ import 'jotai' ╵ ~~~~~~~ node_modules/jotai/package.json:16:13: note: The path "." is not currently exported by package "jotai" 16 │ "exports": { ╵ ^ node_modules/jotai/package.json:18:9: note: None of the conditions provided ("module", "require", "types") match any of the currently active conditions ("browser", "default", "import") 18 │ ".": { ╵ ^ entry.js:1:7: note: Consider using a "require()" call to import this package 1 │ import 'jotai' ╵ ~~~~~~~In this case, one solution could be import this module using
require()since this package provides an export for therequirecondition. Another solution could be to pass--conditions=moduleto esbuild since this package provides an export for themodulecondition (thetypescondition is likely not valid JavaScript code).This problem occurs because this package doesn't provide an import path for ESM code using the
importcondition and also doesn't provide a fallback import path using thedefaultcondition. -
Mention glob syntax in entry point error messages (#976)
In this release, including a
*in the entry point path now causes the failure message to tell you that glob syntax must be expanded first before passing the paths to esbuild. People that hit this are usually converting an existing CLI command to a JavaScript API call and don't know that glob expansion is done by their shell instead of by esbuild. An appropriate fix is to use a library such asglobto expand the glob pattern first before passing the paths to esbuild. -
Raise certain VM versions in the JavaScript feature compatibility table
JavaScript VM feature compatibility data is derived from this dataset: https://kangax.github.io/compat-table/. The scripts that process the dataset expand the data to include all VM versions that support a given feature (e.g.
chrome44,chrome45,chrome46, ...) so esbuild takes the minimum observed version as the first version for which the feature is supported.However, some features can have subtests that each check a different aspect of the feature. In this case the desired version is the minimum version within each individual subtest, but the maximum of those versions across all subtests (since esbuild should only use the feature if it works in all cases). Previously esbuild computed the minimum version across all subtests, but now esbuild computes the maximum version across all subtests. This means esbuild will now lower JavaScript syntax in more cases.
-
Mention the configured target environment in error messages (#975)
Using newer JavaScript syntax with an older target environment (e.g.
chrome10) can cause a build error if esbuild doesn't support transforming that syntax such that it is compatible with that target environment. Previously the error message was generic but with this release, the target environment is called outp explicitly in the error message. This is helpful if esbuild is being wrapped by some other tool since the other tool can obscure what target environment is actually being passed to esbuild. -
Fix an issue with Unicode and source maps
This release fixes a bug where non-ASCII content that ended up in an output file but that was not part of an input file could throw off source mappings. An example of this would be passing a string containing non-ASCII characters to the
globalNamesetting with theminifysetting active and thecharsetsetting set toutf8. The conditions for this bug are fairly specific and unlikely to be hit, so it's unsurprising that this issue hasn't been discovered earlier. It's also unlikely that this issue affected real-world code.The underlying cause is that while the meaning of column numbers in source maps is undefined in the specification, in practice most tools treat it as the number of UTF-16 code units from the start of the line. The bug happened because column increments for outside-of-file characters were incorrectly counted using byte offsets instead of UTF-16 code unit counts.
-
Fix export name annotations in CommonJS output for node (#960)
The previous release introduced a regression that caused a syntax error when building ESM files that have a default export with
--platform=node. This is because the generated export contained thedefaultkeyword like this:0 && (module.exports = {default});. This regression has been fixed.
-
Fix bundling when parent directory is inaccessible (#938)
Previously bundling with esbuild when a parent directory is inaccessible did not work because esbuild would try to read the directory to search for a
node_modulesfolder and would then fail the build when that failed. In practice this caused issues in certain Linux environments where a directory close to the root directory was inaccessible (e.g. on Android). With this release, esbuild will treat inaccessible directories as empty to allow for thenode_modulessearch to continue past the inaccessible directory and into its parent directory. This means it should now be possible to bundle with esbuild in these situations. -
Avoid allocations in JavaScript API stdout processing (#941)
This release improves the efficiency of the JavaScript API. The API runs the binary esbuild executable in a child process and then communicates with it over stdin/stdout. Previously the stdout buffer containing the remaining partial message was copied after each batch of messages due to a bug. This was unintentional and unnecessary, and has been removed. Now this part of the code no longer involves any allocations. This fix was contributed by @jridgewell.
-
Support conditional
@importsyntax when not bundling (#953)Previously conditional CSS imports such as
@import "print.css" print;was not supported at all and was considered a syntax error. With this release, it is now supported in all cases except when bundling an internal import. Support for bundling internal CSS imports is planned but will happen in a later release. -
Always lower object spread and rest when targeting V8 (#951)
This release causes object spread (e.g.
a = {...b}) and object rest (e.g.{...a} = b) to always be lowered to a manual implementation instead of using native syntax when the--target=parameter includes a V8-based JavaScript runtime such aschrome,edge, ornode. It turns out this feature is implemented inefficiently in V8 and copying properties over to a new object is around a 2x performance improvement. In addition, doing this manually instead of using the native implementation generates a lot less work for the garbage collector. You can see V8 bug 11536 for details. If the V8 performance bug is eventually fixed, the translation of this syntax will be disabled again for V8-based targets containing the bug fix. -
Fix object rest return value (#956)
This release fixes a bug where the value of an object rest assignment was incorrect if the object rest assignment was lowered:
// This code was affected let x, y console.log({x, ...y} = {x: 1, y: 2})Previously this code would incorrectly print
{y: 2}(the value assigned toy) when the object rest expression was lowered (i.e. with--target=es2017or below). Now this code will correctly print{x: 1, y: 2}instead. This bug did not affect code that did not rely on the return value of the assignment expression, such as this code:// This code was not affected let x, y ({x, ...y} = {x: 1, y: 2}) -
Basic support for CSS page margin rules (#955)
There are 16 different special at-rules that can be nested inside the
@pagerule. They are defined in this specification. Previously esbuild treated these as unknown rules, but with this release esbuild will now treat these as known rules. The only real difference in behavior is that esbuild will no longer warn about these rules being unknown. -
Add export name annotations to CommonJS output for node
When you import a CommonJS file using an ESM
importstatement in node, thedefaultimport is the value ofmodule.exportsin the CommonJS file. In addition, node attempts to generate named exports for properties of themodule.exportsobject.Except that node doesn't actually ever look at the properties of that object to determine the export names. Instead it parses the CommonJS file and scans the AST for certain syntax patterns. A full list of supported patterns can be found in the documentation for the
cjs-module-lexerpackage. This library doesn't currently support the syntax patterns used by esbuild.While esbuild could adapt its syntax to these patterns, the patterns are less compact than the ones used by esbuild and doing this would lead to code bloat. Supporting two separate ways of generating export getters would also complicate esbuild's internal implementation, which is undesirable.
Another alternative could be to update the implementation of
cjs-module-lexerto support the specific patterns used by esbuild. This is also undesirable because this pattern detection would break when minification is enabled, this would tightly couple esbuild's output format with node and prevent esbuild from changing it, and it wouldn't work for existing and previous versions of node that still have the old version of this library.Instead, esbuild will now add additional code to "annotate" ESM files that have been converted to CommonJS when esbuild's platform has been set to
node. The annotation is dead code but is still detected by thecjs-module-lexerlibrary. If the original ESM file has the exportsfooandbar, the additional annotation code will look like this:0 && (module.exports = {foo, bar});This allows you to use named imports with an ESM
importstatement in node (previously you could only use thedefaultimport):import { foo, bar } from './file-built-by-esbuild.cjs'
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.8.0. See the documentation about semver for more information.
-
Add support for node's
exportsfield inpackage.jsonfiles (#187)This feature was recently added to node. It allows you to rewrite what import paths inside your package map to as well as to prevent people from importing certain files in your package. Adding support for this to esbuild is a breaking change (i.e. code that was working fine before can easily stop working) so adding support for it has been delayed until this breaking change release.
One way to use this feature is to remap import paths for your package. For example, this would remap an import of
your-pkg/esm/lib.js(the "public" import path) toyour-pkg/dist/esm/lib.js(the "private" file system path):{ "name": "your-pkg", "exports": { "./esm/*": "./dist/esm/*", "./cjs/*": "./dist/cjs/*" } }Another way to use this feature is to have conditional imports where the same import path can mean different things in different situations. For example, this would remap
require('your-pkg')toyour-pkg/required.cjsandimport 'your-pkg'toyour-pkg/imported.mjs:{ "name": "your-pkg", "exports": { "import": "./imported.mjs", "require": "./required.cjs" } }There is built-in support for the
importandrequireconditions depending on the kind of import and thebrowserandnodeconditions depending on the current platform. In addition, thedefaultcondition always applies regardless of the current configuration settings and can be used as a catch-all fallback condition.Note that when you use conditions, your package may end up in the bundle multiple times! This is a subtle issue that can cause bugs due to duplicate copies of your code's state in addition to bloating the resulting bundle. This is commonly known as the dual package hazard. The primary way of avoiding this is to put all of your code in the
requirecondition and have theimportcondition just be a light wrapper that callsrequireon your package and re-exports the package using ESM syntax.There is also support for custom conditions with the
--conditions=flag. The meaning of these is entirely up to package authors. For example, you could imagine a package that requires you to configure--conditions=test,en-US. Node has currently only endorsed thedevelopmentandproductioncustom conditions for recommended use. -
Remove the
esbuild.startService()APIDue to #656, Calling
service.stop()no longer does anything, so there is no longer a strong reason for keeping theesbuild.startService()API around. The primary thing it currently does is just make the API more complicated and harder to use. You can now just callesbuild.build()andesbuild.transform()directly instead of callingesbuild.startService().then(service => service.build())oresbuild.startService().then(service => service.transform()).If you are using esbuild in the browser, you now need to call
esbuild.initialize({ wasmURL })and wait for the returned promise before callingesbuild.transform(). It takes the same options thatesbuild.startService()used to take. Note that theesbuild.buildSync()andesbuild.transformSync()APIs still exist when using esbuild in node. Nothing has changed about the synchronous esbuild APIs. -
Remove the
metafilefromoutputFiles(#633)Previously using
metafilewith the API is unnecessarily cumbersome because you have to extract the JSON metadata from the output file yourself instead of it just being provided to you as a return value. This is especially a bummer if you are usingwrite: falsebecause then you need to use a for loop over the output files and do string comparisons with the file paths to try to find the one corresponding to themetafile. Returning the metadata directly is an important UX improvement for the API. It means you can now do this:const result = await esbuild.build({ entryPoints: ['entry.js'], bundle: true, metafile: true, }) console.log(result.metafile.outputs) -
The banner and footer options are now language-specific (#712)
The
--banner=and--footer=options now require you to pass the file type:-
CLI:
esbuild --banner:js=//banner --footer:js=//footer esbuild --banner:css=/*banner*/ --footer:css=/*footer*/ -
JavaScript
esbuild.build({ banner: { js: '//banner', css: '/*banner*/' }, footer: { js: '//footer', css: '/*footer*/' }, }) -
Go
api.Build(api.BuildOptions{ Banner: map[string]string{"js": "//banner"}, Footer: map[string]string{"js": "//footer"}, }) api.Build(api.BuildOptions{ Banner: map[string]string{"css": "/*banner*/"}, Footer: map[string]string{"css": "/*footer*/"}, })
This was changed because the feature was originally added in a JavaScript-specific manner, which was an oversight. CSS banners and footers must be separate from JavaScript banners and footers to avoid injecting JavaScript syntax into your CSS files.
-
-
The extensions
.mjsand.cjsare no longer implicitPreviously the "resolve extensions" setting included
.mjsand.cjsbut this is no longer the case. This wasn't a good default because it doesn't match node's behavior and could break some packages. You now have to either explicitly specify these extensions or configure the "resolve extensions" setting yourself. -
Remove the
--summaryflag and instead just always print a summary (#704)The summary can be disabled if you don't want it by passing
--log-level=warninginstead. And it can be enabled in the API by settinglogLevel: 'info'. I'm going to try this because I believe it will improve the UX. People have this problem with esbuild when they first try it where it runs so quickly that they think it must be broken, only to later discover that it actually worked fine. While this is funny, it seems like a good indication that the UX could be improved. So I'm going to try automatically printing a summary to see how that goes. Note that the summary is not printed if incremental builds are active (this includes the watch and serve modes). -
Rename
--error-limit=to--log-limit=This parameter has been renamed because it now applies to both warnings and errors, not just to errors. Previously setting the error limit did not apply any limits to the number of warnings printed, which could sometimes result in a deluge of warnings that are problematic for Windows Command Prompt, which is very slow to print to and has very limited scrollback. Now the log limit applies to the total number of log messages including both errors and warnings, so no more than that number of messages will be printed. The log usually prints log messages immediately but it will now intentionally hold back warnings when approaching the limit to make room for possible future errors during a build. So if a build fails you should be guaranteed to see an error message (i.e. warnings can't use up the entire log limit and then prevent errors from being printed).
-
Remove the deprecated
--avoid-tdzoptionThis option is now always enabled and cannot be disabled, so it is being removed from the API. The existing API parameter no longer does anything so this removal has no effect the generated output.
-
Remove
SpinnerBusyandSpinnerIdlefrom the Go APIThese options were part of an experiment with the CLI that didn't work out. Watch mode no longer uses a spinner because it turns out people want to be able to interleave esbuild's stderr pipe with other tools and were getting tripped up by the spinner animation. These options no longer do anything and have been removed.
-
Fix overlapping chunk names when code splitting is active (#928)
Code splitting chunks use a content hash in their file name. This is good for caching because it means the file name is guaranteed to change if the chunk contents change, and the file name is guaranteed to stay the same if the chunk contents don't change (e.g. someone only modifies a comment). However, using a pure content hash can cause bugs if two separate chunks end up with the same contents.
A high-level example would be two identical copies of a library being accidentally collapsed into a single copy. While this results in a smaller bundle, this is incorrect because each copy might need to have its own state and so must be represented independently in the bundle.
This release fixes this issue by mixing additional information into the file name hash, which is no longer a content hash. The information includes the paths of the input files as well as the ranges of code within the file that are included in the chunk. File paths are used because they are a stable file identifier, but the relative path is used with
/as the path separator to hopefully eliminate cross-platform differences between Unix and Windows. -
Fix
--keep-namesfor lowered class fieldsAnonymous function expressions used in class field initializers are automatically assigned a
.nameproperty in JavaScript:class Example { field1 = () => {} static field2 = () => {} } assert(new Example().field1.name === 'field1') assert(Example.field2.name === 'field2')This usually doesn't need special handling from esbuild's
--keep-namesoption because esbuild doesn't modify field names, so the.nameproperty will not change. However, esbuild will relocate the field initializer if the configured language target doesn't support class fields (e.g.--target=es6). In that case the.nameproperty wasn't preserved even when--keep-nameswas specified. This bug has been fixed. Now the.nameproperty should be preserved in this case as long as you enable--keep-names. -
Enable importing certain data URLs in CSS and JavaScript
You can now import data URLs of type
text/cssusing a CSS@importrule and import data URLs of typetext/javascriptandapplication/jsonusing a JavaScriptimportstatement. For example, doing this is now possible:import 'data:text/javascript,console.log("hello!");'; import _ from 'data:application/json,"world!"';This is for compatibility with node which supports this feature natively. Importing from a data URL is sometimes useful for injecting code to be evaluated before an external import without needing to generate a separate imported file.
-
Fix a discrepancy with esbuild's
tsconfig.jsonimplementation (#913)If a
tsconfig.jsonfile contains a"baseUrl"value and"extends"anothertsconfig.jsonfile that contains a"paths"value, the base URL used for interpreting the paths should be the overridden value. Previously esbuild incorrectly used the inherited value, but with this release esbuild will now use the overridden value instead. -
Work around the Jest testing framework breaking node's
BufferAPI (#914)Running esbuild within a Jest test fails because Jest causes
Bufferinstances to not be consideredUint8Arrayinstances, which then breaks the code esbuild uses to communicate with its child process. More info is here: https://github.com/facebook/jest/issues/4422. This release contains a workaround that copies eachBufferobject into aUint8Arrayobject when this invariant is broken. That should prevent esbuild from crashing when it's run from within a Jest test. -
Better handling of implicit
mainfields inpackage.jsonIf esbuild's automatic
mainvs.moduledetection is enabled forpackage.jsonfiles, esbuild will now useindex.jsas an implicitmainfield if themainfield is missing butindex.jsis present. This means if apackage.jsonfile only contains amodulefield but not amainfield and the package is imported using both an ESMimportstatement and a CommonJSrequirecall, theindex.jsfile will now be picked instead of the file in themodulefield.
-
Align more closely with node's
defaultimport behavior for CommonJS (#532)Note: This could be considered a breaking change or a bug fix depending on your point of view.
Importing a CommonJS file into an ESM file does not behave the same everywhere. Historically people compiled their ESM code into CommonJS using Babel before ESM was supported natively. More recently, node has made it possible to use ESM syntax natively but to still import CommonJS files into ESM. These behave differently in many ways but one of the most unfortunate differences is how the
defaultexport is handled.When you import a normal CommonJS file, both Babel and node agree that the value of
module.exportsshould be stored in the ESM import nameddefault. However, if the CommonJS file used to be an ESM file but was compiled into a CommonJS file, Babel will set the ESM import nameddefaultto the value of the original ESM export nameddefaultwhile node will continue to set the ESM import nameddefaultto the value ofmodule.exports. Babel detects if a CommonJS file used to be an ESM file by the presence of theexports.__esModule = truemarker.This is unfortunate because it means there is no general way to make code work with both ecosystems. With Babel the code
import * as someFile from './some-file'can access the originaldefaultexport withsomeFile.defaultbut with node you need to usesomeFile.default.defaultinstead. Previously esbuild followed Babel's approach but starting with this release, esbuild will now try to use a blend between the Babel and node approaches.This is the new behavior: importing a CommonJS file will set the
defaultimport tomodule.exportsin all cases except whenmodule.exports.__esModule && "default" in module.exports, in which case it will fall through tomodule.exports.default. In other words: in cases where the default import was previouslyundefinedfor CommonJS files whenexports.__esModule === true, the default import will now bemodule.exports. This should hopefully keep Babel cross-compiled ESM code mostly working but at the same time now enable some node-oriented code to start working.If you are authoring a library using ESM but shipping it as CommonJS, the best way to avoid this mess is to just never use
defaultexports in ESM. Only use named exports with names other thandefault. -
Fix bug when ESM file has empty exports and is converted to CommonJS (#910)
A file containing the contents
export {}is still considered to be an ESM file even though it has no exports. However, if a file containing this edge case is converted to CommonJS internally during bundling (e.g. when it is the target ofrequire()), esbuild failed to mark theexportssymbol from the CommonJS wrapping closure as used even though it is actually needed. This resulted in an output file that crashed when run. Theexportssymbol is now considered used in this case, so the bug has been fixed. -
Avoid introducing
thisfor imported function callsIt is possible to import a function exported by a CommonJS file into an ESM file like this:
import {fn} from './cjs-file.js' console.log(fn())When you do this, esbuild currently transforms your code into something like this:
var cjs_file = __toModule(require("./cjs-file.js")); console.log(cjs_file.fn());However, doing that changes the value of
thisobserved by the exportfn. The property accesscjs_file.fnis in the syntactic "call target" position so the value ofthisbecomes the value ofcjs_file. With this release, esbuild will now use a different syntax in this case to avoid passingcjs_fileasthis:var cjs_file = __toModule(require("./cjs-file.js")); console.log((0, cjs_file.fn)());This change in esbuild mirrors a similar recent TypeScript compiler change, and also makes esbuild more consistent with Babel which already does this transformation.
-
Fix ordering issue with private class methods (#901)
This release fixes an ordering issue with private class fields where private methods were not available inside class field initializers. The issue affected code such as the following when the compilation target was set to
es2020or lower:class A { pub = this.#priv; #priv() { return 'Inside #priv'; } } assert(new A().pub() === 'Inside #priv');With this release, code that does this should now work correctly.
-
Fix
--keep-namesfor private class membersNormal class methods and class fields don't need special-casing with esbuild when the
--keep-namesoption is enabled because esbuild doesn't rename property names and doesn't transform class syntax in a way that breaks method names, so the names are kept without needing to generate any additional code.However, this is not the case for private class methods and private class fields. When esbuild transforms these for
--target=es2020and earlier, the private class methods and private class field initializers are turned into code that uses aWeakMapor aWeakSetfor access to preserve the privacy semantics. This ends up breaking the.nameproperty and previously--keep-namesdidn't handle this edge case.With this release,
--keep-nameswill also preserve the names of private class methods and private class fields. That means code like this should now work with--keep-names --target=es2020:class Foo { #foo() {} #bar = () => {} test() { assert(this.#foo.name === '#foo') assert(this.#bar.name === '#bar') } } -
Fix cross-chunk import paths (#899)
This release fixes an issue with the
--chunk-names=feature where import paths in between two different automatically-generated code splitting chunks were relative to the output directory instead of relative to the importing chunk. This caused an import failure with the imported chunk if the chunk names setting was configured to put the chunks into a subdirectory. This bug has been fixed. -
Remove the guarantee that direct
evalcan access imported symbolsUsing direct
evalwhen bundling is not a good idea because esbuild must assume that it can potentially reach anything in any of the containing scopes. Using directevalhas the following negative consequences:-
All names in all containing scopes are frozen and are not renamed during bundling, since the code in the direct
evalcould potentially access them. This prevents code in all scopes containing the call to directevalfrom being minified or from being removed as dead code. -
The entire file is converted to CommonJS. This increases code size and decreases performance because exports are now resolved at run-time instead of at compile-time. Normally name collisions with other files are avoided by renaming conflicting symbols, but direct
evalprevents symbol renaming so name collisions are prevented by wrapping the file in a CommonJS closure instead. -
Even with all of esbuild's special-casing of direct
eval, referencing an ESMimportfrom directevalstill doesn't necessarily work. ESM imports are live bindings to a symbol from another file and are represented by referencing that symbol directly in the flattened bundle. That symbol may use a different name which could break directeval.
I recently realized that the last consequence of direct
eval(the problem about not being able to referenceimportsymbols) could cause subtle correctness bugs. Specifically esbuild tries to prevent the imported symbol from being renamed, but doing so could cause name collisions that make the resulting bundle crash when it's evaluated. Two files containing directevalthat both import the same symbol from a third file but that import it with different aliases create a system of unsatisfiable naming constraints.So this release contains these changes to address this:
-
Direct
evalis no longer guaranteed to be able to access imported symbols. This means imported symbols may be renamed or removed as dead code even though a call to directevalcould theoretically need to access them. If you need this to work, you'll have to store the relevant imports in a variable in a nested scope and move the call to directevalinto that nested scope. -
Using direct
evalin a file in ESM format is now a warning. This is because the semantics of directevalare poorly understood (most people don't intend to use directevalat all) and because the negative consequences of bundling code with directevalare usually unexpected and undesired. Of the few valid use cases for directeval, it is usually a good idea to rewrite your code to avoid using directevalin the first place.For example, if you write code that looks like this:
export function runCodeWithFeatureFlags(code) { let featureFlags = {...} eval(code) // "code" should be able to access "featureFlags" }you should almost certainly write the code this way instead:
export function runCodeWithFeatureFlags(code) { let featureFlags = {...} let fn = new Function('featureFlags', code) fn(featureFlags) }This still gives
codeaccess tofeatureFlagsbut avoids all of the negative consequences of bundling code with directeval.
-
-
Support chunk and asset file name templates (#733, #888)
This release introduces the
--chunk-names=and--asset-names=flags. These flags let you customize the output paths for chunks and assets within the output directory. Each output path is a template and currently supports these placeholders:[name]: The original name of the file. This will bechunkfor chunks and will be the original file name (without the extension) for assets.[hash]: The content hash of the file. This is not necessarily stable across different esbuild versions but will be stable within the same esbuild version.
For example, if you want to move all chunks and assets into separate subdirectories, you could use
--chunk-names=chunks/[name]-[hash]and--asset-names=assets/[name]-[hash]. Note that the path template should not include the file extension since the file extension is always automatically added to the end of the path template.Additional name template features are planned in the future including a
[dir]placeholder for the relative path from theoutbasedirectory to the original input directory as well as an--entry-names=flag, but these extra features have not been implemented yet. -
Handle
thisin class static field initializers (#885)When you use
thisin a static field initializer inside aclassstatement or expression, it references the class object itself:class Foo { static Bar = class extends this { } } assert(new Foo.Bar() instanceof Foo)This case previously wasn't handled because doing this is a compile error in TypeScript code. However, JavaScript does allow this so esbuild needs to be able to handle this. This edge case should now work correctly with this release.
-
Do not warn about dynamic imports when
.catch()is detected (#893)Previously esbuild avoids warning about unbundled
import()expressions when using thetry { await import(_) }pattern, since presumably thetryblock is there to handle the run-time failure of theimport()expression failing. This release adds some new patterns that will also suppress the warning:import(_).catch(_),import(_).then(_).catch(_), andimport(_).then(_, _). -
CSS namespaces are no longer supported
CSS namespaces are a weird feature that appears to only really be useful for styling XML. And the world has moved on from XHTML to HTML5 so pretty much no one uses CSS namespaces anymore. They are also complicated to support in a bundler because CSS namespaces are file-scoped, which means:
-
Default namespaces can be different in different files, in which case some default namespaces would have to be converted to prefixed namespaces to avoid collisions.
-
Prefixed namespaces from different files can use the same name, in which case some prefixed namespaces would need to be renamed to avoid collisions.
Instead of implementing all of that for an extremely obscure feature, CSS namespaces are now just explicitly not supported. The code to handle
@namespacehas been removed from esbuild. This will likely not affect anyone, especially because bundling code using CSS namespaces with esbuild didn't even work correctly in the first place. -
-
Fix a concurrent map write with the
--inject:feature (#878)This release fixes an issue where esbuild could potentially crash sometimes with a concurrent map write when using injected files and entry points that were neither relative nor absolute paths. This was an edge case where esbuild's low-level file subsystem was being used without being behind a mutex lock. This regression was likely introduced in version 0.8.21. The cause of the crash has been fixed.
-
Provide
kindtoonResolveplugins (#879)Plugins that add
onResolvecallbacks now have access to thekindparameter which tells you what kind of import is being resolved. It will be one of the following values:-
"entry-point"in JS (api.ResolveEntryPointin Go)An entry point provided by the user
-
"import-statement"in JS (api.ResolveJSImportStatementin Go)A JavaScript
importorexportstatement -
"require-call"in JS (api.ResolveJSRequireCallin Go)A JavaScript call to
require(...)with a string argument -
"dynamic-import"in JS (api.ResolveJSDynamicImportin Go)A JavaScript
import(...)expression with a string argument -
"require-resolve"in JS (api.ResolveJSRequireResolvein Go)A JavaScript call to
require.resolve(...)with a string argument -
"import-rule"in JS (api.ResolveCSSImportRulein Go)A CSS
@importrule -
"url-token"in JS (api.ResolveCSSURLTokenin Go)A CSS
url(...)token
These values are pretty much identical to the
kindfield in the JSON metadata file. -
-
The stderr log format now contains line numbers after file names (#865)
Error messages in stderr now have a line and column number after the file name.
Before:
> src/structs/RTree.js: warning: Duplicate key "compareMinX" in object literal 469 │ compareMinX: function (a, b) ╵ ~~~~~~~~~~~ src/structs/RTree.js: note: The original "compareMinX" is here 206 │ compareMinX: compareNodeMinX, ╵ ~~~~~~~~~~~After:
> src/structs/RTree.js:469:4: warning: Duplicate key "compareMinX" in object literal 469 │ compareMinX: function (a, b) ╵ ~~~~~~~~~~~ src/structs/RTree.js:206:4: note: The original "compareMinX" is here 206 │ compareMinX: compareNodeMinX, ╵ ~~~~~~~~~~~This should make log messages slightly easier to parse if you want to parse stderr instead of using esbuild's API. Previously you needed a multi-line regular expression to get the line number, but now that the line number is duplicated in two places you should only need a single-line regular expression.
Note that this is still the hacky way to get error information and is potentially unstable, since it will break if the log format changes. Log messages are mainly intended for humans. The straightforward and stable way to do this is still to use esbuild's API, which returns log messages as an array of objects.
-
Allow
--definewithimport.metaThe
--definefeature lets you replace specific identifiers and member expression chains with compile-time constants. However, it previously didn't work withimport.metabecause this is a special case in the grammar. Theimportkeyword is not actually an identifier expression. This distinction isn't helpful though, and it's not unreasonable to want to use the--definefeature to replaceimport.metaproperties too.With this release, it's now possible to use e.g.
--define:import.meta.foo=123to replace specific properties accessed off of theimport.metaobject as well as to use e.g.--define:import.meta={\"foo\":123}to substitute the entireimport.metaexpression with something else. -
Fix a race condition with multiple injected files (#871)
Using multiple injected files could cause a data race that trips Go's race detector. The data race has been fixed in this release. The fix was contributed by @Deleplace.
-
Change
--servebehavior to serve on all interfaces (#866)The default address for the
--serveflag has changed from127.0.0.1(serve on the loopback interface) to0.0.0.0(serve on all interfaces). You can still manually specify either one using--serve=127.0.0.1:8000or--serve=0.0.0.0:8000. This just changes the default behavior that happens when you pass--servewith no host address (or when you just use the--servedir=flag without--serve=).In addition, you can now also specify an IPv6 address. Previously there was a parsing issue that prevented this. For example, you can pass
--serve=[::1]:8000to serve on the loopback interface and--serve=[::]:8000to serve on all interfaces. -
Change the import resolution rules of absolute paths (#862)
Previously absolute paths were considered to be pre-resolved by the resolver (in contrast to relative and package paths, which need to be converted to an absolute path). This meant that absolute paths which did not actually exist caused a failure in the loader when it tried to load the path instead of in the resolver when it tried to resolve the path.
With the previous change in version 0.8.47 to support removing URL query and/or hash parameters from the path, path resolution can now be run multiple times. If path resolution fails and the path contains a
?and/or#, path resolution is re-run with the URL query/hash parameters removed. It is problematic to consider absolute paths to be pre-resolved because it means that paths containing query/hash parameters make the loader try to load the wrong path, and do not run the resolver again with the parameter suffix removed.In this release, esbuild will now validate absolute paths in the resolver. So invalid paths will now fail in the resolver and retry without the parameter suffix instead of failing in the loader, which correctly handles a parameter suffix on absolute paths. In addition, this release now handles implicit file extensions on absolute paths. This makes esbuild a more accurate copy of node's module resolution algorithm, which does this as well.
-
Output files in
metafilenow haveentryPoint(#711)There is now an optional
entryPointproperty on each output file in the JSON metadata file generated with the--metafile=flag. It is only present for output files that are the bundled results of entry point files, and contains the path name of the corresponding input entry point file. This property is not present on other kinds of output files (e.g. code splitting chunks). This feature was contributed by @remorses.
-
Using direct
evalnow pulls inmoduleandexportsUse of direct
evalforces the file to become a CommonJS module and disables dead code elimination in the entire file. The CommonJS closure is necessary to avoid name collisions with other modules, sinceevalmeans symbols in the file can no longer be renamed to avoid collisions.However, the CommonJS
moduleandexportsvariables that are arguments to the closure previously weren't considered to be used in this scenario, meaning they may be omitted as dead code for size reasons. This could cause code insideevalto behave incorrectly. Now use of directevalautomatically counts as a use of bothmoduleandexportsso these variables should now always be present in this case. -
Always remove all
"use asm"directives (#856)The asm.js subset of JavaScript has complicated validation rules that are triggered by this directive. The parser and code generator in esbuild was not designed with asm.js in mind and round-tripping asm.js code through esbuild will very likely cause it to no longer validate as asm.js. When this happens, V8 prints a warning and people don't like seeing the warning. The warning looks like this:
(node:58335) V8: example.js:3 Invalid asm.js: Unexpected token (Use `node --trace-warnings ...` to show where the warning was created)I am deliberately not attempting to preserve the validity of asm.js code because it's a complicated legacy format and it's obsolete now that WebAssembly exists. By removing all
"use asm"directives, the code will just become normal JavaScript and work fine without generating a warning. -
Fix a variable hoisting edge case (#857)
It is allowed to use a nested
varhoisted declaration with the same name as a top-level function declaration. In that case the two symbols should merge and be treated as the same symbol:async function x() {} { var x; }The parser previously allowed this for regular functions but not for async or generator functions. Now with this release, this behavior is also allowed for these special kinds of functions too.
-
Remove empty CSS rules when minifying (#851)
Empty rules with no content such as
div {}are now removed when CSS is minified. This change was contributed by @susiwen8.
-
Work around a problem with
pnpmandNODE_PATH(#816)In version 0.8.43, esbuild added support for node's
NODE_PATHenvironment variable which contains a list of global folders to use during path resolution. However, this causes a problem when esbuild is installed with pnpm, an alternative JavaScript package manager. Specifically pnpm adds a bogus path toNODE_PATHthat doesn't exist but that has a file as a parent directory. Previously this caused esbuild to fail with the errornot a directory. Now with this release, esbuild will ignore this bogus path instead of giving an error. -
Add more names to the global no-side-effect list (#842)
This release adds almost all known globals from the browser and node to the list of known globals. Membership in this list means accessing the global is assumed to have no side effects. That means tree shaking is allowed to remove unused references to these globals. For example, since
HTMLElementis now in the known globals list, the following class will now be removed when unused:class MyElement extends HTMLElement { }In addition, membership in this list relaxes ordering constraints for the purposes of minification. It allows esbuild to reorder references to these globals past other expressions. For example, since
console.logis now in the known globals list, the following simplification will now be performed during minification:// Original export default (a) => { if (a) console.log(b); else console.log(c) } // Minified (previous release) export default (a) => { a ? console.log(b) : console.log(c); }; // Minified (this release) export default (a) => { console.log(a ? b : c); };This transformation is not generally safe because the
console.logproperty access might evaluate code which could potentially change the value ofa. This is only considered safe in this instance becauseconsole.logis now in the known globals list.Note that membership in this list does not say anything about whether the function has side effects when called. It only says that the identifier has no side effects when referenced. So
console.log()is still considered to have side effects even thoughconsole.logis now considered to be free of side effects.The following globals are not on the list and are considered to have side effects:
scrollXscrollYinnerWidthinnerHeightpageXOffsetpageYOffsetlocalStoragesessionStorage
Accessing layout-related properties can trigger a layout and accessing storage-related properties can throw an exception if certain privacy settings are enabled. Both of these behaviors are considered side effects.
-
Fix a TypeScript parser regression (#846)
Restrictions on array and object destructuring patterns in the previous release introduced a regression where arrays or objects in TypeScript code could fail to parse if they were wrapped in a double layer of parentheses. This was due to the speculative parsing of arrow function arguments. The regression has been fixed.
-
Add the Go-specific
cli.ParseServeOptions()API (#834)This API is specifically for people trying to emulate esbuild's CLI in Go. It lets you share esbuild's logic of parsing the
--serve=and--servedir=flags. Use it like this:serveOptions, args, err := cli.ParseServeOptions([]string{ "--serve=8000", }) buildOptions, err := cli.ParseBuildOptions(args) result := api.Serve(serveOptions, buildOptions)
-
Fix some parsing edge cases (#835)
This release fixes the following edge cases:
-
Code using
ininside a template literal inside a for loop initializer such asfor (let x = `${a in b ? '0' : '1'}`; false; );is now allowed. Previously theinoperator was incorrectly considered to be part of a for-in loop. -
In TypeScript, it's not valid to have a newline in between the
asyncand the<tokens inside the codeasync <T>() => {}. Previously this was incorrectly treated as an asynchronous arrow function expression. -
Code of the form
new async()must construct the function calledasync. Previously this was incorrectly treated asnew (async())()instead due to the speculative parsing of asynchronous arrow functions. -
Code of the form
new async () => {}must not be allowed. Previously this was incorrectly allowed since the speculative parsing of asynchronous arrow functions did not check the precedence level. -
It's not valid to start an initializer expression in a for-of loop with the token
letsuch asfor (let.foo of bar) {}. This is now forbidden. In addition, the code generator now respects this rule sofor ((let.foo) of bar) {}is now printed asfor ((let).foo of bar) {}. -
Array and object binding patterns do not allow a comma after rest elements, so code such as
[...a, b] = [c]is invalid. This case is correctly handled by esbuild. However, it's possible to have both an array or object binding pattern and an array or object literal on the left-hand side of a destructuring assignment such as[[...a, b].c] = [d]. In that case it should be allowed for a comma to come after the spread element in the array or object literal expression. Previously this was incorrectly treated as an error by esbuild. -
It's technically allowed (although perhaps not ever actually useful) to call
super()from within a default argument initializer like this:class Derived extends Base { constructor(arg = super()) { } }Previously esbuild did not permit this, which is incorrect. Doing this is now permitted.
-
It is an error to use
argumentsin a class field initializer such asclass { x = arguments[0] }, but it is not an error to useargumentsin a computed class property name such asclass { [arguments[0]] = x }or inside TypeScript decorators such asclass { @decorator(arguments[0]) x() {} }. Previously all of these cases were an error in esbuild, which is incorrect. Usingargumentsinside computed class property names and TypeScript decorators is now allowed. -
It is not permitted to use a function declaration inside an if statement such as
if (0) function f() {}in strict mode. Previously this was allowed, but this is now forbidden. -
It is not permitted to re-declare a generator and/or asynchronous function declaration inside a block scope:
// This is allowed function *a() {} function *a() {} // This is allowed function f() { function *b() {} function *b() {} } // This is not allowed { function *c() {} function *c() {} }The parser now enforces this rule.
-
Legacy octal escape sequences are octal escape sequences other than
\0with a single zero. These are forbidden in untagged template literals and in all strings in strict mode code. Previously esbuild didn't enforce this rule, but it is now enforced. -
Technically the directive prologue is allowed to contain multiple directives, so strict mode should still be applied even if a
"use strict";directive is preceded by another directive. For example,"use \000"; "use strict";should be a syntax error because strict mode is active. This technicality has now been implemented. -
It is supposed to be a syntax error if a use strict directive is inside a function with a non-simple parameter list, such as
(x = 1) => { 'use strict' }. Previously esbuild allowed this code, but now this code is a syntax error. -
It is forbidden for a template literal tag to be an optional chain such as
a?.b`c`. This rule is now enforced by esbuild, so code like this is now a syntax error. In addition, the code generator now avoids generating this syntax by wrapping any optional chain template literal tags in parentheses. -
According to the standard, all code inside a class statement or expression should be in strict mode. Previously esbuild treated code inside a class as the same strict mode status as the surrounding code, but now code in a class is always interpreted as strict mode code.
-
Duplicate bindings in the same parameter list are not allowed if the parameter list isn't simple, such as in the code
function f(a, [a]) {}, or if the parameter list belongs to an arrow function or a method. This rule is now enforced by esbuild's parser, so doing this is now a syntax error. -
Array and object destructuring patterns are only valid if they are not surrounded by parentheses. Previously esbuild incorrectly allowed code such as
([]) = []and({}) = {}. This invalid code is now a syntax error. -
It is now an error to use the shorthand property syntax
({yield})inside a generator and({await})inside an asynchronous function. Previously those cases were incorrectly allowed. -
A newline in between
asyncand a method name is no longer allowed. Instead, this is a syntax error inside an object literal and a class field inside a class body.
-
-
Remove the local web server feature from the WebAssembly package (#836)
This feature didn't work anyway (maybe sockets don't work with Go's WebAssembly target?) and including it added around 3mb of unnecessary extra code to the WebAssembly module file. Removing this brings the size of the WebAssembly module from around 11mb down to 8.3mb.
-
Release native binaries for the Apple M1 chip (#550)
Previously installing esbuild on a M1 actually installed the x86-64 version, which required the Rosetta 2 translator. This was because Go hadn't yet released support for the M1. Now that Go 1.16.0 has been released, esbuild can support the M1 natively. It's supported by esbuild starting with this release. There are reports of the native version being 1.4x faster than the translated version. This change was contributed by @rtsao.
-
Omit warning about
require.somePropertywhen targeting CommonJS (#812)The
require.cacheproperty allows introspecting the state of therequirecache, generally without affecting what is imported/bundled.Since esbuild's static analyzer only detects direct calls to
require, it currently warns about uses ofrequirein any situation other than a direct call since that means the value is "escaping" the analyzer. This is meant to detect and warn about indirect calls such as['fs', 'path'].map(require).However, this warning is not relevant when accessing a property off of the
requireobject such asrequire.cachebecause a property access does not result in capturing the value ofrequire. Now a warning is no longer generated forrequire.somePropertywhen the output format iscjs. This allows for the use of features such asrequire.cacheandrequire.extensions. This fix was contributed by @huonw. -
Support ignored URL parameters at the end of import paths (#826)
If path resolution fails, ebuild will now try again with the URL query and/or fragment removed. This helps handle ancient CSS code like this that contains hacks for Internet Explorer:
@font-face { src: url("./themes/default/assets/fonts/icons.eot?#iefix") format('embedded-opentype'), url("./themes/default/assets/fonts/icons.woff2") format('woff2'), url("./themes/default/assets/fonts/icons.woff") format('woff'), url("./themes/default/assets/fonts/icons.ttf") format('truetype'), url("./themes/default/assets/fonts/icons.svg#icons") format('svg'); }Previously path resolution would fail because these files do not end with the
.eot?#iefixor.svg#iconsextensions. Now path resolution should succeed. The URL query and fragment are not unconditionally stripped because there is apparently code in the wild that uses#as a directory name. So esbuild will still try to resolve the full import path first and only try to reinterpret the path as a URL if that fails. -
Prevent paths starting with
/from being used as relative paths on Windows (#822)On Windows, absolute paths start with a drive letter such as
C:\...instead of with a slash like/.... This means that paths starting with a/can actually be used as relative paths. For example, this means an import of/subfolder/image.pngwill match the file at the path./subfolder/image.png. This is problematic for Windows users because they may accidentally make use of these paths and then try to run their code on a non-Windows platform only for it to fail to build.Now paths starting with a
/are always treated as an absolute path on all platforms. This means you can no longer import files at a relative path that starts with/on Windows. You should be using a./prefix instead. -
Warn when importing a path with the wrong case
Importing a path with the wrong case (e.g.
File.jsinstead offile.js) will work on Windows and sometimes on macOS because they have case-insensitive file systems, but it will never work on Linux because it has a case-sensitive file system. To help you make your code more portable and to avoid cross-platform build failures, esbuild now issues a warning when you do this.
-
Fix minification of
.0in CSS (#804)If you write
.0instead of0in CSS and enabled--minify, esbuild would previously minify this token incorrectly (the token was deleted). This bug has been fixed and esbuild should now minify this token to0. -
Support range requests in local HTTP server
The local HTTP server built in to esbuild now supports range requests, which are necessary for video playback in Safari. This means you can now use
<video>tags in your HTML pages with esbuild's local HTTP server.
-
Add the
--servedir=flag (#796)The
--serveflag starts a local web server and serves the files that would normally be written to the output directory. So for example if you had an entry point calledsrc/app.tsand an output directory of--outdir=www/js, using esbuild with--servewould expose the generated output file via http://localhost:8000/app.js (but not write anything towww/js). This can then be used in combination with your normal development server (running concurrently on another port) by adding<script src="http://localhost:8000/app.js"></script>in your HTML file. So esbuild with the--serveflag is meant to augment your normal development server, not replace it.This release introduces a new
--servedir=flag which gives you the option of replacing your normal development server with esbuild. The directory you pass here will be "underlayed" below the output directory. Specifically when an incoming HTTP request comes in esbuild will first check if it matches one of the generated output files and if so, serve the output file directly from memory. Otherwise esbuild will fall back to serving content from the serve directory on the file system. In other words, server's URL structure behaves like a normal file server in a world where esbuild had written the generated output files to the file system (even though the output files actually only exist in memory).So for example if you had an entry point called
src/app.tsand an output directory of--outdir=www/js, using esbuild with--servedir=wwwwould expose the entire contents of thewwwdirectory via http://localhost:8000/ except for the http://localhost:8000/js/app.js URL which would contain the compiled contents ofsrc/app.ts. This lets you have awww/index.htmlfile containing just<script src="/js/app.js"></script>and use one web server instead of two.The benefit of doing things this way is that you can use the exact same HTML pages in development and production. In development you can run esbuild with
--servedir=and esbuild will serve the generated output files directly. For production you can omit that flag and esbuild will write the generated files to the file system. In both cases you should be getting the exact same result in the browser with the exact same code in both development and production.This will of course not support all workflows, but that's intentional. This is designed to be a quality-of-life improvement for the simple case of building a small static website with some HTML, JavaScript, and CSS. More advanced setups may prefer to avoid the
--servedir=feature and e.g. configure a NGINX reverse proxy to esbuild's local server to integrate esbuild into a larger existing development setup.One unintended consequence of this feature is that esbuild can now be used as a general local HTTP server via
esbuild --servedir=.. Without any entry points, esbuild won't actually build anything and will just serve files like a normal web server. This isn't the intended use case but it could perhaps be a useful side effect of this feature. -
Remove absolute paths for disabled packages from source maps (#786)
This change is similar to the one from the previous release for disabled files, but it applies to package paths instead of relative paths. It's relevant when using packages that override dependencies with alternative packages using the
browserfield in theirpackage.jsonfile. Using relative paths instead of absolute paths fixes a determinism issue where build output was different on different systems. This fix was contributed by @eelco. -
Handle absolute paths in
tsconfig.json(#792)Some automatically-generated
tsconfig.jsonpaths can have absolute paths in them. This is allowed by the TypeScript compiler (specifically in thepathsandextendsfields). With this release, esbuild now supports absolute paths inpathsandextendstoo. -
Change the watch mode output format (#793)
Previously esbuild would print a "..." animation to the console while watch mode was scanning for changes. The intent of this was to a) not take up too much space in the terminal and b) show that esbuild's watch mode isn't frozen. Since the release I have gotten feedback that this isn't desirable. People want more feedback about what's happening and want to be able to run regexes over the stderr stream instead of using esbuild's actual API.
This release changes the output format for watch mode. Now esbuild will print
[watch] build startedwhen watch mode triggers a rebuild and[watch] build finishedwhen the rebuild is complete. Any build errors will be printed in between those two log messages.Note that this means esbuild's watch mode output is now more verbose, especially when there are frequent file changes. If you want to hide these new messages you can use
--log-level=with a level other thaninfo.
-
Create a logo for esbuild (#61)
This release introduces a logo for esbuild:
Inspirations for the logo include:
-
The fast-forward symbol because esbuild is extremely fast and because one of esbuild's goals is to accelerate the evolution of the whole web tooling ecosystem.
-
The right-shift symbol because esbuild's production optimizations make your code smaller and because esbuild itself contains many low-level optimizations for speed.
Having a logo for esbuild should make it easier to include esbuild in lists of other tools since the other tools often all have logos.
-
-
Add support for node's
--preserve-symlinksflag (#781)This release adds the
--preserve-symlinksflag which behaves like the corresponding flag in node. Without the flag, esbuild and node will use the real path (after resolving symlinks) as the identity of a file. This means that a given file can only be instantiated once. With the flag, esbuild and node will use the original path (without resolving symlinks) as the identity of a file. This means that a given file can be instantiated multiple times, once for every symlink pointing to it. Each copy will have its own identity so the resulting bundle may contain duplicate files. This option is useful if your code relies on this flag in node (or theresolve.symlinkssetting in Webpack). -
Ignore a leading byte order mark (BOM) in CSS files (#776)
Some text editors insert a U+FEFF code point at the start of text files. This is a zero-width non-breaking space character. Using one at the start of a file is a convention which is meant to indicate that the contents of the file are UTF-8 encoded. When this is done, the character is called a byte order mark.
Unlike JavaScript, CSS does not treat U+FEFF as whitespace. It is treated as an identifier instead. This was causing esbuild to misinterpret files starting with a BOM as starting with an extra identifier, which could then cause the initial CSS rule in the file to be parsed incorrectly.
Now esbuild will skip over a BOM if it's present before beginning to parse CSS. This should prevent issues when working with these files.
-
Add message notes to the API
The internal logging system has the ability to attach additional notes to messages to provide more information. These show up as additional log messages in the terminal when using the command-line interface. Here is an example of a note:
> src/structs/RTree.js: warning: Duplicate key "compareMinX" in object literal 469 │ compareMinX: function (a, b) ╵ ~~~~~~~~~~~ src/structs/RTree.js: note: The original "compareMinX" is here 206 │ compareMinX: compareNodeMinX, ╵ ~~~~~~~~~~~With this release, notes are also supported in the JS and Go APIs. This means you can now generate your own notes using plugins as well as inspect the notes generated by esbuild.
-
Add origin information to errors from plugins (#780)
Errors thrown during JavaScript plugin callback evaluation will now be annoated to show where that plugin callback was registered. That looks like this:
> example-plugin.js: error: [example-plugin] foo.bar is not a function 15 │ foo.bar(); ╵ ^ at ./example-plugin.js:15:13 at ./node_modules/esbuild/lib/main.js:750:34 example-plugin.js: note: This error came from the "onLoad" callback registered here 13 │ build.onLoad({ filter: /.*/ }, args => { ╵ ~~~~~~ at setup (./example-plugin.js:13:13) at handlePlugins (./node_modules/esbuild/lib/main.js:668:7)This should make it easier to debug crashes in plugin code.
-
Fix a regression with the synchronous JavaScript API (#784)
In version 0.8.39, a change was made to avoid dangling esbuild processes when node exits abnormally. The change introduced a periodic ping between the child esbuild process and its host process. If the ping doesn't go through, the child process is able to detect that the host process is no longer there. Then it knows to exit since it's no longer being used.
This caused a problem with the synchronous JavaScript API calls which run the esbuild child process in a single-response mode. The ping message was interpreted as a second response and tripped up the message protocol. Pings are only useful for the asynchronous API calls. Running the pings during synchronous API calls was unintentional. With this release pings are no longer run for synchronous API calls so this regression should be fixed.
-
Remove absolute paths for disabled files from source maps (#785)
Files can be ignored (i.e. set to empty) using the
browserfield inpackage.json. Specifically, you can set thebrowserfield to a map where the key is the module name and the value isfalse. This is a convention followed by several bundlers including esbuild.Previously ignoring a file caused that file's path to appear as an absolute path in any generated source map. This is problematic because it means different source maps will be generated on different systems, since the absolute path contains system-specific directory information. Now esbuild will treat these paths the same way it treats other paths and will put a relative path in the source map.
-
Support the
XDG_CACHE_HOMEenvironment variable (#757)On Linux, the install script for esbuild currently caches downloaded binary executables in
~/.cache/esbuild/bin. This change means esbuild will now try installing to$XDG_CACHE_HOME/esbuild/bininstead of theXDG_CACHE_HOMEenvironment variable exists. This allows you to customize the cache directory on Linux. The specification that definesXDG_CACHE_HOMEis here. -
Further improve constant folding of branches (#765)
At a high level, this release adds the following substitutions to improve constant folding and dead code elimination:
if (anything && falsyWithSideEffects)→if (anything, falsyWithSideEffects)if (anything || truthyWithSideEffects)→if (anything, truthyWithSideEffects)if (anything && truthyNoSideEffects)→if (anything)if (anything || falsyNoSideEffects)→if (anything)if (anything, truthyOrFalsy)→anything; if (truthyOrFalsy)
And also these substitutions for unused expressions:
primitive == primitive→primitive, primitivetypeof identifier→ (remove entirely)
The actual substitutions are more complex since they are more comprehensive but they essentially result in this high-level behavior. Note that these substitutions are only done when minification is enabled.
-
Fix an edge case with CSS variable syntax (#760)
CSS variables are whitespace-sensitive even though other CSS syntax is mostly not whitespace sensitive. It is apparently common for this to cause problems with CSS tooling that pretty-prints and minifies CSS, including esbuild before this release. Some examples of issues with other tools include postcss/postcss#1404 and tailwindlabs/tailwindcss#2889. The issue affects code like this:
div { --some-var: ; some-decl: var(--some-var, ); }It would be a change in semantics to minify this code to either
--some-var:;orvar(--some-var,)due to the whitespace significance of CSS variables, so such transformations are invalid. With this release, esbuild should now preserve whitespace in these two situations (CSS variable declarations and CSS variable references). -
Add support for recursive symlinks during path resolution (#766)
Previously recursive symlinks (a symlink that points to another symlink) were an unhandled case in the path resolution algorithm. Now these cases should be supported up to a depth of 256 symlinks. This means esbuild's path resolution should now work with multi-level
yarn linkscenarios. -
Fix subtle circular dependency issue (#758)
If esbuild is used to transform TypeScript to JavaScript without bundling (i.e. each file is transformed individually), the output format is CommonJS, and the original TypeScript code contains an import cycle where at least one of the links in the cycle is an
export * asre-export statement, there could be certain situations where evaluating the transformed code results in an import beingundefined. This is caused by the__esModulemarker being added after the call torequire()for the first transformed re-export statement. The fix was to move the marker to before the first call torequire(). The__esModulemarker is a convention from Babel that esbuild reuses which marks a module as being originally in the ECMAScript module format instead of the CommonJS module format. -
Add support for the
NODE_PATHenvironment variableThis is a rarely-used feature of Node's module resolution algorithm. From the documentation:
If the
NODE_PATHenvironment variable is set to a colon-delimited list of absolute paths, then Node.js will search those paths for modules if they are not found elsewhere.On Windows,
NODE_PATHis delimited by semicolons (;) instead of colons.The CLI takes the list of node paths from the value of the
NODE_PATHenvironment variable, but the JS and Go APIs take the list as an array of strings instead (callednodePathsin JS andNodePathsin Go).
-
Fix crash with block-level function declaration and
--keep-names(#755)This release fixes a crash with block-level function declarations and the
--keep-namesoption. The crash affected code that looks like this:if (true) function f() {} assert.strictEqual(f.name, 'f') -
Disallow additional features in strict mode
This change improves esbuild's compliance with the JavaScript specification. It is now an error to use legacy octal numeric literals and the identifiers
implements,interface,let,package,private,protected,public,static, andyieldin strict mode code. -
Basic support for watch mode with plugins (#752)
With this release, watch mode should now work with simple on-load plugins. Watch mode is implemented by tracking all file system accesses made by esbuild as it does a build. However, this doesn't catch external file system accesses such as those made by plugins. Now if an on-load plugin is used on a path in the
filenamespace, esbuild will also read the file during watch mode so that watch mode is aware of the file system access. Note that there is not yet API support for a plugin to return additional paths for watch mode to monitor. -
Make JavaScript API error format more consistent (#745)
If a JavaScript error is thrown while validating the build options, the thrown error should now have
errorsandwarningsproperties just like normal build errors. Previously these properties were only present if the build itself failed but not if build options were invalid. This consistency should make it easier to process errors from the build API call.
-
Fix memory leak with watch mode when using the CLI (#750)
This release fixes a memory leak when using
--watchfrom the CLI (command-line interface). When esbuild was in this state, every incremental build resulted in more memory being consumed. This problem did not affect users of the JS API or Go API, only users of the CLI API.The problem was that the GC (garbage collector) was disabled. Oops. This is done by default for speed when you use esbuild via the CLI, which makes sense for most CLI use cases because the process is usually short-lived and doesn't need to waste time cleaning up memory. But it does not make sense for flags that cause esbuild to be a long-running process.
Previously the only exception to this rule was the
--serveflag. When I added watch mode, I forgot to enable GC for the--watchflag too. With this release, the GC is enabled for both the--serveand the--watchflags so esbuild should no longer leak memory in watch mode. -
Special-case certain syntax with
--format=esm(#749)You can now no longer use the following syntax features with the
esmoutput format:- The
withstatement:with (x) {} - Delete of a bare identifier:
delete x
In addition, the following syntax feature is transformed when using the
esmoutput format:- For-in variable initializers:
for (var x = y in {}) {}→x = y; for (var x in {}) {}
The reason is because all JavaScript engines interpret code in the
esmoutput format as strict mode and these syntax features are disallowed in strict mode. Note that this new strict mode handling behavior in esbuild is only dependent on the output format. It does not depend on the presence or absence of"use strict"directives. - The
-
Basic
"use strict"trackingThe JavaScript parser now tracks
"use strict"directives and propagates strict mode status through the code. In addition, files containing theimportand/orexportkeywords are also considered to be in strict mode. Strict mode handling is complex and esbuild currently doesn't implement all strict mode checks. But the changes in this release are a starting point. It is now an error to use certain syntax features such as awithstatement within a strict mode scope. -
Fix a minifier bug with
withstatementsThe minifier removes references to local variables if they are unused. However, that's not correct to do inside a
withstatement scope because what appears to be an identifier may actually be a property access, and property accesses could have arbitrary side effects if they resolve to a getter or setter method. Now all identifier expressions insidewithstatements are preserved when minifying. -
Transform block-level function declarations
Block-level function declarations are now transformed into equivalent syntax that avoids block-level declarations. Strict mode and non-strict mode have subtly incompatible behavior for how block-level function declarations are interpreted. Doing this transformation prevents problems with code that was originally strict mode that is run as non-strict mode and vice versa.
Now esbuild uses the presence or absence of a strict mode scope to determine how to interpret the block-level function declaration and then converts it to the equivalent unambiguous syntax such that it works the same regardless of whether or not the current scope is in strict mode:
// This original code: while (!y) { function y() {} } // is transformed into this code in strict mode: while (!y) { let y2 = function() {}; } // and into this code when not in strict mode: while (!y) { let y2 = function() {}; var y = y2; }
-
Fix TypeScript parameter decorators on class constructors (#734)
This release fixes a TypeScript translation bug where parameter decorators on class constructors were translated incorrectly. Affected code looks like this:
class Example { constructor(@decorator param: any) {} }This bug has been fixed. In addition, decorators are no longer allowed on class constructors themselves because they are not allowed in TypeScript.
-
Resolve
browserentries inpackage.jsonwith no file extension (#740)This fix changes how esbuild interprets the
browserfield inpackage.json. It will now remap imports without a file extension tobrowsermap entries without a file extension, which improves compatibility with Webpack. Specifically, apackage.jsonfile with"browser": {"./file": "./something.js"}will now match an import of./file. Previously thepackage.jsonfile had to contain something like"browser": {"./file.js": "./something.js"}instead. Note that for compatibility with the rest of the ecosystem, a remapping of./filewill counter-intuitively not match an import of./file.jseven though it works fine in the other direction. -
Warning: npm v7 bug may prevent esbuild installation
This is a warning for people reading these release notes, not a code change. I have discovered a bug in npm v7 where your
package-lock.jsonfile can become corrupted such that nopostinstallscripts are run. This bug affects all packages withpostinstallscripts, not just esbuild, and happens when running npm v7 on apackage-lock.jsonfile from npm v6 or earlier. It seems like deleting and regenerating yourpackage-lock.jsonfile is a valid workaround that should get esbuild working again.
-
Fix the JavaScript watch mode API exiting early (#730)
The previous release contained a bug that caused the JavaScript watch mode API to exit early in some cases. This bug should now be fixed. The problem was caused by some code that shouldn't even need to exist now that you are no longer required to call
stop()on an esbuild service created bystartService()(it was made optional in version 0.8.32). I took the opportunity to clean up the internals of esbuild's JavaScript API implementation which ended up removing the entire section of code that contained this bug. -
Add an API option for a per-build working directory (#689)
You can now use the
absWorkingDirAPI option to customize the current working directory. It will default to the value ofprocess.cwd()at the time of the call tostartService()when not specified, which matches the existing behavior. The working directory is used for a few different things including resolving relative paths given as API options to absolute paths and pretty-printing absolute paths as relative paths in log messages.In addition to being a useful feature, this change also simplifies esbuild's internals. Previously esbuild had to maintain separate child processes if the current working directory was changed in between build API calls. Now esbuild will always reuse the same child process across all build API calls. The
stop()call on thestartService()API is also now a no-op (it doesn't do anything anymore) and thestartService()API may be removed in future releases. -
Fix stray
esbuildprocess afternodeexits (#643)I discovered that using esbuild's JavaScript incremental build API could result in the child
esbuildprocess not exiting when the parentnodeprocess exits. This was due to a reference counting issue. The bug has been fixed so this shouldn't happen anymore.
-
Implement a simple cross-platform watch mode (#21)
With this release, you can use the
--watchflag to run esbuild in watch mode which watches the file system for changes and does an incremental build when something has changed. The watch mode implementation uses polling instead of OS-specific file system events for portability.Note that it is still possible to implement watch mode yourself using esbuild's incremental build API and a file watcher library of your choice if you don't want to use a polling-based approach. Also note that this watch mode feature is about improving developer convenience and does not have any effect on incremental build time (i.e. watch mode is not faster than other forms of incremental builds).
The new polling system is intended to use relatively little CPU vs. a traditional polling system that scans the whole directory tree at once. The file system is still scanned regularly but each scan only checks a random subset of your files to reduce CPU usage. This means a change to a file will be picked up soon after the change is made but not necessarily instantly. With the current heuristics, large projects should be completely scanned around every 2 seconds so in the worst case it could take up to 2 seconds for a change to be noticed. However, after a change has been noticed the change's path goes on a short list of recently changed paths which are checked on every scan, so further changes to recently changed files should be noticed almost instantly.
-
Add
pluginDatato pass data between plugins (#696)You can now return additional data from a plugin in the optional
pluginDatafield and it will be passed to the next plugin that runs in the plugin chain. So if you return it from anonLoadplugin, it will be passed to theonResolveplugins for any imports in that file, and if you return it from anonResolveplugin, an arbitrary one will be passed to theonLoadplugin when it loads the file (it's arbitrary since the relationship is many-to-one). This is useful to pass data between different plugins without them having to coordinate directly.
-
Improve ambiguous import handling (#723)
It is an error to try to import a name from a file where there are multiple matching exports due to multiple
export * fromstatements from files which export that name. This release contains a few improvements to ambiguous import handling:-
This release fixes a bug where named export shadowing didn't work correctly with multiple levels of re-exports. A named export closer in the re-export chain is supposed to hide a named export deeper in the re-export chain without causing an ambiguous import. The bug caused this case to be incorrectly flagged as an error even though it should have been allowed. This case is now allowed without an error.
-
Previously the error message just said that there was an ambiguous import but didn't have any additional information. With this release, the error message also points out where the two different exports that have collided are in their original source files. Hopefully this should make it quicker to diagnose these types of issues.
-
Real JavaScript environments only treat ambiguous imports as an error if they are explicitly a named import. Using the
import * assyntax and then accessing the ambiguous import with a property access results inundefinedinstead of an error. Previously esbuild also treated this case as an error because it automatically rewrites star-import syntax to named-import syntax to improve tree shaking. With this release, this case is now treated as a warning instead of an error and the import will be automatically replaced with anundefinedliteral in the bundled code.
-
-
Reuse automatically-generated temporary
*.nodefiles (#719)The previous change to hide the automatically-generated N-API native node extensions from Yarn 2 writes these
*.nodefiles to the system's temporary directory. A new one was being created on each run which is wasteful even though they are only a few kilobytes in size. With this release*.nodefiles will now be reused if they are already present in the system's temporary directory, so a new one is no longer created on each run. This fix was contributed by @kzc. -
Fix the serve API with
outfile(#707)This release fixes a bug where the serve API did not work with the
outfilesetting. Using this setting with the serve API should now work fine. -
Warn about duplicate keys in object literals
Using a duplicate key in an object literal such as
{x: 1, x: 2}is now a warning. This is allowed in JavaScript but results in subsequent keys overwriting the previous key. It's usually a copy/paste error and isn't ever useful so it's worth warning about. -
Avoid generating duplicate keys in JSON metadata
The
outputmap that is generated when themetafilefeature is active could potentially have duplicate keys if thefileloader is used, there are multiple entry points, and two or more entry points reference the same file. This is harmless because both keys mapped to the same value, but it's confusing and unnecessary. Duplicate keys are no longer present in the output map in this latest release. -
Make the JSON metafile structure match the type definitions (#726)
Previously
importsand/orexportscould be missing from entries in theoutputmap in certain cases (specifically for source maps and files loaded with thefileloader). This was problematic because the TypeScript type definitions for the metafile say that theimportsandexportsproperties are non-optional. With this release, theimportsandexportsproperties are now always present so the existing TypeScript type definitions are now accurate. -
Update from Go 1.15.5 to Go 1.15.7
The version of Go used to build the released binary executables on npm is now Go 1.15.7. This change shouldn't result in any visible changes to esbuild. It was only upgraded because the Go extension for the VSCode IDE now uses the official
goplsGo language service and this extension wanted the latest version of Go.
-
Fix an issue with writing large files to stdout using the WebAssembly executable
The previous release introduced a regression where large output files written to stdout were incorrectly truncated when using the WebAssembly
esbuildcommand. This regression was due to a missing callback to the JavaScriptwrite()function when called on the stdout stream. The regression has been fixed. -
Hide the N-API native node extensions from Yarn 2
The previous release introduced some very small (1-2kb)
*.nodenative extensions to fix a bug with node failing to exit properly. However, this causes Yarn 2 to unzip the esbuild package, which is undesirable. This release puts these native node extensions inside JavaScript code instead to hide them from Yarn 2. The native extensions are written to a temporary file at run-time if necessary.
-
Fix a commonly-missed corner case with
awaitinside**I recently discovered an interesting discussion about JavaScript syntax entitled "Most implementations seem to have missed that
await x ** 2is not legal". Indeed esbuild has missed this, but this is not surprising because V8 has missed this as well and I usually test esbuild against V8 to test if esbuild is conformant with the JavaScript standard. Regardless, it sounds like the result of the discussion is that the specification should stay the same and implementations should be fixed. This release fixes this bug in esbuild's parser. The syntaxawait x ** 2is no longer allowed and parentheses are now preserved for the syntax(await x) ** 2. -
Allow namespaced names in JSX syntax (#702)
XML-style namespaced names with a
:in the middle are a part of the JSX specification but they are explicitly unimplemented by React and TypeScript so esbuild doesn't currently support them. However, there was a user request to support this feature since it's part of the JSX specification and esbuild's JSX support can be used for non-React purposes. So this release now supports namespaced names in JSX expressions:let xml = <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> <rdf:Description rdf:ID="local-record"> <dc:title>Local Record</dc:title> </rdf:Description> </rdf:RDF>This JSX expression is now transformed by esbuild to the following JavaScript:
let xml = React.createElement("rdf:RDF", { "xmlns:rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", "xmlns:dc": "http://purl.org/dc/elements/1.1/" }, React.createElement("rdf:Description", { "rdf:ID": "local-record" }, React.createElement("dc:title", null, "Local Record")));Note that if you are trying to namespace your React components, this is not the feature to use. You should be using a
.instead of a:for namespacing your React components since.resolves to a JavaScript property access. -
Fix
worker: falsein esbuild's browser-based JavaScript APIThe browser-based JavaScript API creates a web worker by default but this can be disabled by passing
worker: false. When you do this the WebAssembly code is run in the current thread which will lock up the thread. This is mainly useful if you're calling the JavaScript API from within a web worker and you want to avoid creating another nested web worker.This option was unintentionally broken when the internal JavaScript web worker source code was moved from an inline function to a string in version 0.5.20. The regression has been fixed and the
worker: falsescenario now has test coverage. -
Fix absolute paths with the
esbuild-wasmpackage on Windows (#687)The package
esbuild-wasmhas anesbuildcommand implemented using WebAssembly instead of using native code. It uses node's WebAssembly implementation and calls methods on node'sfsmodule to access the file system.Go's
path/filepathmodule has a bug where Windows paths are interpreted as Unix paths when targeting WebAssembly: golang/go#43768. This causes multiple issues including absolute paths such asC:\path\to\file.jsbeing interpreted as relative paths (since they don't start with a/) and being joined onto the end of other paths.To fix this, esbuild now does all of its own path handling instead of using Go's path handling code. The esbuild code base now contains a forked copy of
path/filepaththat can handle both Windows and Unix paths. The decision about which one to use is made at run-time. When targeting WebAssembly, the presence of theC:\directory is used to determine if Windows-style paths should be used.With this release, it should now be possible to use Windows-style paths with esbuild's WebAssembly implementation on Windows.
-
Fix using stdin with the
esbuild-wasmpackage on Windows (#687)Node has an old bug (nodejs/node#19831, nodejs/node#35997) where
fs.readreturns an EOF error at the end of stdin on Windows. This causes Go's WebAssembly implementation to panic when esbuild tries to read from stdin.The workaround was to manually check for this case and then ignore the error in this specific case. With this release, it should now be possible to pipe something to the
esbuildcommand on Windows. -
Fix stdout and stderr not supporting Unicode in the
esbuild-wasmpackage on Windows (#687)Node's
fs.writeAPI is broken when writing Unicode to stdout and stderr on Windows, and this will never be fixed: nodejs/node#24550. This is problematic for Go's WebAssembly implementation because it uses this API for writing to all file descriptors.The workaround is to manually intercept the file descriptors for stdout and stderr and redirect them to
process.stdoutandprocess.stderrrespectively. Passing Unicode text towrite()on these objects instead of on thefsAPI strangely works fine. So with this release, Unicode text should now display correctly when using esbuild's WebAssembly implementation on Windows (or at least, as correctly as the poor Unicode support in Windows Command Prompt allows). -
Add a hack for faster command-line execution for the WebAssembly module in certain cases
Node has an unfortunate bug where the node process is unnecessarily kept open while a WebAssembly module is being optimized: https://github.com/nodejs/node/issues/36616. This means cases where running
esbuildshould take a few milliseconds can end up taking many seconds instead.The workaround is to force node to exit by ending the process early. This is done in one of two ways depending on the exit code. For non-zero exit codes (i.e. when there is a build error), the
esbuildcommand now callsprocess.kill(process.pid)to avoid the hang.For zero exit codes, the
esbuildcommand now loads a N-API native node extension that calls the operating system'sexit(0)function. This is done without requiringnode-gypby precompiling each supported platform and just including all of them in theesbuild-wasmpackage since they are so small. If this hack doesn't work in certain cases, the process should exit anyway just potentially many seconds later. Currently the only supported platforms for this hack are 64-bit macOS, Windows, and Linux. -
Fix non-absolute paths with the
esbuild-wasmpackage in the browser (#693)When using esbuild in the browser via WebAssembly, it was not possible to specify an non-absolute output path. Normally you can do this and esbuild will just convert it to an absolute path by resolving it as a relative path from the current working directory. However, Go's WebAssembly implementation has no current working directory so the conversion operation to an absolute path failed, causing esbuild's API to fail.
With this release, esbuild should now behave as if the current working directory is
/in the browser. For example, this means calling thebuild()API withoutfile: 'file.js'should now generate an output file called/file.jsinstead of causing an error.
-
Fix a parser bug about suffix expressions after an arrow function body (#701)
The JavaScript parser incorrectly handled suffix expressions after a non-expression arrow function body. In practice, this came up when a semicolon was omitted from the end of an expression statement and the following expression could be considered a suffix expression:
x = () => {} (y)This was incorrectly parsed as
(x = () => {})(y);instead ofx = () => {}; y;. With this release, this edge case should now be parsed correctly. -
Add new
neutralplatform to help text (#695)The new
--platform=neutralAPI option that was added in the previous release was incorrectly not listed in the CLI help text for the platform feature. This omission has been fixed. The fix was contributed by @hardfist.
-
Fix esbuild potentially exiting early during incremental rebuilds
The change in the previous release to make calling
stop()optional caused a regression for incremental rebuilds where callingrebuild()could potentially cause the process to exit early before the incremental rebuild is completed. This is because the implementation ofrebuild()was missing a reference count to track that the service is now temporarily needed again. This omission was an oversight, and has now been fixed. -
Fix using the new
sourcesContentoption with the transform API (#682)Due to an oversight, the
sourcesContent: falseoption that was added in version 0.8.27 didn't work with the JavaScript transform API. This was unintentional and has been fixed. This fix was contributed by @jschaf. -
Insert the object spread shim in constructor methods after the
super()call (#678)This fixes an issue with the transform for object spread to older compile targets. Previously the following code would be transformed to code that crashes when run if the compile target is
es2017or lower:class Derived extends Base { prop = null; constructor({ ...args }) { super(args); } }This code was incorrectly compiled to something like this, which will throw
ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor:class Derived extends Base { constructor(_a) { __publicField(this, "prop", null); var args = __rest(_a, []); super(args); } }With this release, it will now be compiled to something like this instead:
class Derived extends Base { constructor(_a) { var args = __rest(_a, []); super(args); __publicField(this, "prop", null); } } -
Add the
--platform=neutralAPI option (#674)There are currently two platform values:
browser(the default) andnode. These settings are a convenient way to configure multiple defaults for other API options for maximum compatibility. However, some users want to configure everything themselves so esbuild does not assume any platform-specific behavior. In this case you can now use--platform=neutralto disable platform-specific default values. Note that this means if you want to use npm-style packages you will have to configure a main field yourself with something like--main-fields=main. -
Provide minified and non-minified versions of in-browser API library (#616)
The in-browser JavaScript API libraries for esbuild are in the esbuild-wasm package. There are two:
esbuild-wasm/lib/browser.jsin UMD format andesbuild-wasm/esm/browser.jsin ESM format. Previously these were minified since they contain a large string of JavaScript that cannot be minified by other tools. Now they are no longer minified, and there are new minified versions available atesbuild-wasm/lib/browser.min.jsandesbuild-wasm/esm/browser.min.js.
-
Calling
stop()on the JavaScript API is now optional (#656)The JavaScript implementation of esbuild's API now calls
unref()internally so node will now exit even if the internal long-lived esbuild process is still running. You should no longer need to explicitly callstop()on the service returned bystartService(), which simplifies service lifetime management. This feature was contributed by @SalvatorePreviti. -
Fix bug in metafile path generation (#662)
Certain import path metadata in the JSON file generated by the
--metafilesetting could be incorrect in scenarios with code splitting active and multiple entry points in different subdirectories. The incorrect paths referred to cross-chunk imports of other generated code splitting chunks and were incorrectly relative to the subdirectory inside the output directory instead of relative to the output directory itself. This issue has been fixed. -
Add
kindto import paths in metafile JSON (#655)The
--metafileflag generates build metadata in JSON format describing the input and output files in the build. Previously import path objects only had apathproperty. With this release, they now also have akindproperty that describes the way the file was imported. The value is a string that is equal to one of the following values:For JavaScript files:
import-statementrequire-calldynamic-importrequire-resolve
For CSS files:
import-ruleurl-token
-
Add support for TypeScript 4.2 syntax
Most of the new features included in the TypeScript 4.2 beta announcement are type system features that don't apply to esbuild. But there's one upcoming feature that adds new syntax:
abstractconstruct signatures. They look like this:let Ctor: abstract new () => HasArea = Shape;This new syntax can now be parsed by esbuild.
-
Add
detailto errors and warnings (#654)Errors and warnings returned by the JavaScript and Go APIs now have a
detailproperty which contains the original error. This is relevant if a custom JavaScript exception is thrown or a custom Goerroris returned from inside a plugin callback. -
Disable code warnings inside
node_modulesdirectories even with plugins (#666)Some of the warnings that esbuild generates exist to point out suspicious looking code that is likely a bug. An example is
typeof x == 'null'since thetypeofoperator never generates the stringnull. Arguably these warnings belong in a linter instead of in esbuild since esbuild is a bundler, but I figured that some warnings about obviously broken code would still be helpful because many people don't run linters. It's part of my quest to improve software quality. And these warnings have caught real bugs in published code so they aren't meaningless. The warning must be considered very unlikely to be a false positive to be included.A change was added in version 0.7.4 to exclude files inside
node_modulesdirectories from these warnings. Even if the warnings flag a real bug, the warning is frustrating as a user because it's mostly non-actionable. The only resolution other than turning off warnings is to file an issue with the package, since code in published packages is immutable.However, since then the plugin API has been released and this behavior didn't apply if the import path was resolved by a plugin. It only applied if the import path was resolved by esbuild itself. That problem is fixed in this release. Now these warnings will be omitted from any file with
node_modulesin its path, even if the path originated from a plugin. -
Remove the warning about self-assignment (#666)
This warning was added in version 0.8.11 and warns about self-assignment such as
x = x. The rationale is that this is likely a copy/paste error. However, it triggers too often for cross-compiled TypeScript code so the false positive rate is too high. The warning has now been removed. -
Disable constant folding for the
?:operator when not minifying (#657)When minification is not enabled, the
?:operator will now no longer be simplified if the condition evaluates totrueorfalse. This could result in slower builds in certain cases because esbuild may now scan more files unnecessarily during bundling. This change was made because of a user request.
-
Fix minification issue from previous release (#648)
The minification optimization to omit certain
continueandreturnstatements when it's implied by control flow in version 0.8.29 caused a regression when the branch condition uses a hoisted function:if (fn()) return; ... function fn() {}In that case, transforming the code by inverting the condition and moving the following statements inside the branch is not valid because the function is no longer hoisted to above the branch condition. This release fixes the regression by avoiding this optimization in cases like this.
-
Add the option
--sourcemap=both(#650)This new option puts the generated source map both an inline
//# sourceMappingURL=data URL comment inside the output file and in an external file next to the output file. Using it is also possible with the transform API, which will cause it to return both an inline data URL comment in thecodevalue and the source map JSON in themapvalue. -
Tree-shake unused code with
--format=iife(#639)When the output format is IIFE (which wraps the code in an immediately-invoked function expression), esbuild now assumes that it's safe to remove unused code. This is an assumption that esbuild always makes when bundling but that esbuild previously didn't make when not bundling. Now esbuild will remove code even when not bundling as long as the output format is IIFE.
This is only done for the IIFE output format because people are currently using the other formats to compile "partial modules", meaning they expect to be able to append code to esbuild's output and have that appended code be able to reference unused code inside esbuild's output. So it's not safe for esbuild to remove unused code in those cases. The IIFE output format wraps everything in a closure so unused code is not exposed to the module-level scope. Appended code will not be able to access unused code inside the closure so that means it's safe to remove.
-
Fix
@jsxand@jsxFragcomments without trailing spacesThe
--jsx-factoryand--jsx-fragmentsettings can be set on a per-file basis using// @jsx nameor// @jsxFrag namecomments. Comments of the form/* @jsx name */or/* @jsxFrag name */will also work. However, there was a bug where comments of the form/* @jsx name*/or/* @jsxFrag name*/(a multi-line comment without a trailing space at the end) did not work. This bug has been fixed, and you now no longer need a trailing space for multi-line comments. -
Minification improvements
-
The expression before a switch statement is now folded into the value. This means
fn(); switch (x) { ... }turns intoswitch (fn(), x) { ... }. -
Uses of
===and!==are converted to==or!=if the types of both sides can easily be statically determined. This means(x & 1) === 0turns into(x & 1) == 0. -
Equality comparisons are removed if both sides are boolean and one side is a constant. This means
!x === trueturns into!x. -
Certain unary and binary operators are now removed if unused. This means
if (a() === b()) {}turns intoa(), b();. -
The comma operator is now extracted from certain expressions. This means
(a, b) + cturns intoa, b + c. -
Minification now takes advantage of the left-associativity of certain operators. This means
a && (b && c)turns intoa && b && c. -
Computed properties that are strings now become no longer computed. This means
{['a']: b}turns into{a: b}andclass { ['a'] = b }turns intoclass { a = b }. -
Repeated if-jump statements are now merged. This means
if (a) break; if (b) break;turns intoif (a || b) break;.
-
-
Fix issues with nested source maps (#638)
A nested source map happens when an input file has a valid
//# sourceMappingURL=comment that points to a valid source map file. In that case, esbuild will read that source map and use it to map back to the original source code from the generated file. This only happens if you enable source map generation in esbuild via--sourcemap. This release fixes the following issues:-
Generated source maps were incorrect when an input file had a nested source map and the input source map had more than one source file. This regression was introduced by an optimization in version 0.8.25 that parallelizes the generation of certain internal source map data structures. The index into the generated
sourcesarray was incorrectly incremented by 1 for every input file instead of by the number of sources in the input source map. This issue has been fixed and now has test coverage. -
Generated source maps were incorrect when an input file had a nested source map, the file starts with a local variable, the previous file ends with a local variable of that same type, and the input source map is missing a mapping at the start of the file. An optimization was added in version 0.7.18 that splices together local variable declarations from separate files when they end up adjacent to each other in the generated output file (i.e.
var a=0;var b=2;becomesvar a=0,b=2;whenaandbare in separate files). The source map splicing was expecting a mapping at the start of the file and that isn't necessarily the case when using nested source maps. The optimization has been disabled for now to fix source map generation, and this specific case has test coverage.
-
-
Allow entry points outside of the
outbasedirectory (#634)When esbuild generates the output path for a bundled entry point, it computes the relative path from the
outbasedirectory to the input entry point file and then joins that relative path to the output directory. For example, if there are two entry pointssrc/pages/home/index.tsandsrc/pages/about/index.ts, the outbase directory issrc, and the output directory isout, the output directory will containout/pages/home/index.jsandout/pages/about/index.js.However, this means that the
outbasedirectory is expected to contain all entry point files (even implicit entry point files fromimport()expressions). If an entry point isn't under the outbase directory then esbuild will to try to write the output file outside of the output directory, since the path of the entry point relative tooutbasewill start with../which is then joined to the output directory. This is unintentional. All output files are supposed to be written inside of the output directory.This release fixes the problem by creating a directory with the name
_.._in the output directory for output file paths of entry points that are not inside theoutbasedirectory. So if the previous example was bundled with an outbase directory oftemp, the output directory will containout/_.._/pages/home/index.jsandout/_.._/pages/about/index.js. Doing this instead of stripping the leading../off the relative path is necessary to avoid collisions between different entry points with the same path suffix. -
Minification improvements
This release contains the following minification improvements:
-
Expressions of the form
!(a == b)are now converted toa != b. This also applies similarly for the other three equality operators. -
A trailing
continue;statement inside the body of a loop is now removed. -
Minification can now omit certain
continueandreturnstatements when it's implied by control flow:// Before minification function fn() { if (a) return; while (b) { if (c) continue; d(); } } // After minification function fn() { if (!a) for (; b; ) c || d(); } -
Certain single-use variables are now inlined if the use directly follows the variable:
// Before minification let result = fn(); let callback = result.callback; return callback.call(this);// After minification return fn().callback.call(this);This transformation is only done when it's safe to do so. The safety conditions are complex but at a high level, an expression cannot be reordered past another expression if either of them could possibly have side effects.
-
-
Add a
--summaryflag that prints helpful information after a build (#631)Normally esbuild's CLI doesn't print anything after doing a build if nothing went wrong. This allows esbuild to be used as part of a more complex chain of tools without the output cluttering the terminal. However, sometimes it is nice to have a quick overview in your terminal of what the build just did. You can now add the
--summaryflag when using the CLI and esbuild will print a summary of what the build generated. It looks something like this:$ ./esbuild --summary --bundle src/Three.js --outfile=build/three.js --sourcemap build/three.js 1.0mb ⚠️ build/three.js.map 1.8mb ⚡ Done in 43ms -
Keep unused imports in TypeScript code in one specific case (#604)
The official TypeScript compiler always removes imported symbols that aren't used as values when converting TypeScript to JavaScript. This is because these symbols could be types and not removing them could result in a run-time module instantiation failure because of missing exports. This even happens when the
tsconfig.jsonsetting"importsNotUsedAsValues"is set to"preserve". Doing this just keeps the import statement itself but confusingly still removes the imports that aren't used as values.Previously esbuild always exactly matched the behavior of the official TypeScript compiler regarding import removal. However, that is problematic when trying to use esbuild to compile a partial module such as when converting TypeScript to JavaScript inside a file written in the Svelte programming language. Here is an example:
<script lang="ts"> import Counter from './Counter.svelte'; export let name: string = 'world'; </script> <main> <h1>Hello {name}!</h1> <Counter /> </main>The current Svelte compiler plugin for TypeScript only provides esbuild with the contents of the
<script>tag so to esbuild, the importCounterappears to be unused and is removed.In this release, esbuild deliberately deviates from the behavior of the official TypeScript compiler if all of these conditions are met:
-
The
"importsNotUsedAsValues"field intsconfig.jsonmust be present and must not be set to"remove". This is necessary because this is the only case where esbuild can assume that all imports are values instead of types. Any imports that are types will cause a type error when the code is run through the TypeScript type checker. To import types when theimportsNotUsedAsValuessetting is active, you must use the TypeScript-specificimport typesyntax instead. -
You must not be using esbuild as a bundler. When bundling, esbuild needs to assume that it's not seeing a partial file because the bundling process requires renaming symbols to avoid cross-file name collisions.
-
You must not have identifier minification enabled. It's useless to preserve unused imports in this case because referencing them by name won't work anyway. And keeping the unused imports would be counter-productive to minification since they would be extra unnecessary data in the output file.
This should hopefully allow esbuild to be used as a TypeScript-to-JavaScript converter for programming languages such as Svelte, at least in many cases. The build pipeline in esbuild wasn't designed for compiling partial modules and this still won't be a fully robust solution (e.g. some variables may be renamed to avoid name collisions in rare cases). But it's possible that these cases are very unlikely to come up in practice. Basically this change to keep unused imports in this case should be useful at best and harmless at worst.
-
-
Mark
import.metaas supported in node 10.4+ (#626)It was previously marked as unsupported due to a typo in esbuild's compatibility table, which meant esbuild generated a shim for
import.metaeven when it's not necessary. It should now be marked as supported in node 10.4 and above so the shim will no longer be included when using a sufficiently new target environment such as--target=node10.4. -
Fix for when the working directory ends with
/(#627)If the working directory ended in
/, the last path component would be incorrectly duplicated. This was the case when running esbuild with Yarn 2 (but not Yarn 1) and is problematic because some externally-facing directories reference the current working directory in plugins and in output files. The problem has now been fixed and the last path component is no longer duplicated in this case. This fix was contributed by @remorses. -
Add an option to omit
sourcesContentfrom generated source maps (#624)You can now pass
--sources-content=falseto omit thesourcesContentfield from generated source maps. The field embeds the original source code inline in the source map and is the largest part of the source map. This is useful if you don't need the original source code and would like a smaller source map (e.g. you only care about stack traces and don't need the source code for debugging). -
Fix exports from ESM files converted to CJS during code splitting (#617)
This release fixes an edge case where files in ECMAScript module format that are converted to CommonJS format during bundling can generate exports to non-top-level symbols when code splitting is active. These files must be converted to CommonJS format if they are referenced by a
require()call. When that happens, the symbols in that file are placed inside the CommonJS wrapper closure and are no longer top-level symbols. This means they should no longer be considered exportable for cross-chunk export generation due to code splitting. The result of this fix is that these cases no longer generate output files with module instantiation errors. -
Allow
--definewith array and object literals (#581)The
--definefeature allows you to replace identifiers such asDEBUGwith literal expressions such asfalse. This is valuable because the substitution can then participate in constant folding and dead code elimination. For example,if (DEBUG) { ... }could becomeif (false) { ... }which would then be completely removed in minified builds. However, doing this with compound literal expressions such as array and object literals is an anti-pattern because it could easily result in many copies of the same object in the output file.This release adds support for array and object literals with
--defineanyway, but they work differently than other--defineexpressions. In this case a separate virtual file is created and configured to be injected into all files similar to how the--injectfeature works. This means there is only at most one copy of the value in a given output file. However, these values do not participate in constant folding and dead code elimination, since the object can now potentially be mutated at run-time.
-
Ensure the current working directory remains unique per
startService()callThe change in version 0.8.24 to share service instances caused problems for code that calls
process.chdir()before callingstartService()to be able to get a service with a different working directory. With this release, calls tostartService()no longer share the service instance if the working directory was different at the time of creation. -
Consider import references to be side-effect free (#613)
This change improves tree shaking for code containing top-level references to imported symbols such as the following code:
import {Base} from './base' export class Derived extends Base {}Identifier references are considered side-effect free if they are locally-defined, but esbuild special-cases identifier references to imported symbols in its AST (the identifier
Basein this example). This meant they did not trigger this check and so were not considered locally-defined and therefore side-effect free. That meant thatDerivedin this example would never be tree-shaken.The reason for this is that the side-effect determination is made during parsing and during parsing it's not yet known if
./baseis a CommonJS module or not. If it is, thenBasewould be a dynamic run-time property access onexports.Basewhich could hypothetically be a property with a getter that has side effects. Therefore it could be considered incorrect to remove this code due to tree-shaking because there is technically a side effect.However, this is a very unlikely edge case and not tree-shaking this code violates developer expectations. So with this release, esbuild will always consider references to imported symbols as being side-effect free. This also aligns with ECMAScript module semantics because with ECMAScript modules, it's impossible to have a user-defined getter for an imported symbol. This means esbuild will now tree-shake unused code in cases like this.
-
Warn about calling an import namespace object
The following code is an invalid use of an import statement:
import * as express from "express" express()The
expresssymbol here is an import namespace object, not a function, so calling it will fail at run-time. This code should have been written like this instead:import express from "express" express()This comes up because for legacy reasons, the TypeScript compiler defaults to a compilation mode where the
import * asstatement is converted toconst express = require("express")which means you can actually callexpress()successfully. Doing this is incompatible with standard ECMAScript module environments such as the browser, node, and esbuild because an import namespace object is never a function. The TypeScript compiler has a setting to disable this behavior calledesModuleInteropand they highly recommend applying it both to new and existing projects to avoid these compatibility problems. See the TypeScript documentation for more information.With this release, esbuild will now issue a warning when you do this. The warning indicates that your code will crash when run and that your code should be fixed.
-
Fix a performance regression from version 0.8.4 specific to Yarn 2
Code using esbuild's
transformSyncfunction via Yarn 2 experienced a dramatic slowdown in esbuild version 0.8.4 and above. This version added a wrapper script to fix Yarn 2's incompatibility with binary packages. Some code that tries to avoid unnecessarily calling into the wrapper script contained a bug that caused it to fail, which meant that usingtransformSyncwith Yarn 2 called into the wrapper script unnecessarily. This launched an extra node process every time the esbuild executable was invoked which can be over 6x slower than just invoking the esbuild executable directly. This release should now invoke the esbuild executable directly without going through the wrapper script, which fixes the performance regression. -
Fix a size regression from version 0.7.9 with certain source maps (#611)
Version 0.7.9 added a new behavior to esbuild where in certain cases a JavaScript file may be split into multiple pieces during bundling. Pieces of the same input file may potentially end up in multiple discontiguous regions in the output file. This was necessary to fix an import ordering bug with CommonJS modules. However, it had the side effect of duplicating that file's information in the resulting source map. This didn't affect source map correctness but it made source maps unnecessarily large. This release corrects the problem by ensuring that a given file's information is only ever represented once in the corresponding source map.
-
Share reference-counted service instances internally (#600)
Now calling
startService()multiple times will share the underlying esbuild child process as long as the lifetimes of the service objects overlap (i.e. the time fromstartService()toservice.stop()). This is just an internal change; there is no change to the public API. It should result in a faster implementation that uses less memory if your code callsstartService()multiple times. Previously each call tostartService()generated a separate esbuild child process. -
Fix re-exports of a side-effect free CommonJS module (#605)
This release fixes a regression introduced in version 0.8.19 in which an
importof anexport {...} fromre-export of a CommonJS module does not include the CommonJS module if it has been marked as"sideEffect": falsein itspackage.jsonfile. This was the case with the Ramda library, and was due to an unhandled case in the linker. -
Optionally take binary executable path from environment variable (#592)
You can now set the
ESBUILD_BINARY_PATHenvironment variable to cause the JavaScript API to use a different binary executable path. This is useful if you want to substitute a modified version of theesbuildbinary that contains some extra debugging information.
-
Fix non-string objects being passed to
transformSync(#596)The transform function is only supposed to take a string. The type definitions also specify that the input must be a string. However, it happened to convert non-string inputs to a string and some code relied on that behavior. A change in 0.8.22 broke that behavior for
transformSyncspecifically forUint8Arrayobjects, which became an array of numbers instead of a string. This release ensures that the conversion to a string is done up front to avoid something unexpected happening in the implementation. Future releases will likely enforce that the input is a string and throw an error otherwise. -
Revert the speedup to
transformSyncandbuildSync(#595)This speedup relies on the
worker_threadsmodule in node. However, when esbuild is used vianode -ras innode -r esbuild-register file.ts, the worker thread created by esbuild somehow ends up being completely detached from the main thread. This may be a bug in node itself. Regardless, the approach esbuild was using to improve speed doesn't work in all cases so it has been reverted. It's unclear if it's possible to work around this issue. This approach for improving the speed of synchronous APIs may be a dead end.
-
Escape fewer characters in virtual module paths (#588)
If a module's path is not in the
filenamespace (i.e. it was created by a plugin), esbuild doesn't assume it's a file system path. The meaning of these paths is entirely up to the plugin. It could be anything including a HTTP URL, a string of code, or randomly-generated characters.Currently esbuild generates a file name for these virtual modules using an internal "human-friendly identifier" that can also be used as a valid JavaScript identifier, which is sometimes used to for example derive the name of the default export of a bundled module. But that means virtual module paths which do happen to represent file system paths could cause more characters to be escaped than necessary. For example, esbuild escapes
-to_because-is not valid in a JavaScript identifier.This release separates the file names derived from virtual module paths from the internal "human-friendly identifier" concept. Characters in the virtual module path that are valid in file paths are no longer escaped.
In the future the output file name of a virtual module will likely be completely customizable with a plugin, so it will be possible to have different behavior for this if desired. But that isn't possible quite yet.
-
Speed up the JavaScript
buildSyncandtransformSyncAPIs (#590)Previously the
buildSyncandtransformSyncAPI calls created a new child esbuild process on every call because communicating with a long-lived child process is asynchronous in node. However, there's a trick that can work around this limitation: esbuild can communicate with the long-lived child process from a child thread using node'sworker_threadsmodule and block the main thread using JavaScript's new Atomics API. This was a tip from @cspotcode.This approach has now been implemented. A quick benchmark shows that
transformSyncis now 1.5x to 15x faster than it used to be. The speedup depends on the size of the input (smaller inputs get a bigger speedup). The worker thread and child process should automatically be terminated when there are no more event handlers registered on the main thread, so there is no explicitstop()call like there is with a service object. -
Distribute a 32-bit Linux ARM binary executable via npm (#528)
You should now be able to use npm to install esbuild on a 32-bit Linux ARM device. This lets you run esbuild on a Raspberry Pi. Note that this target isn't officially supported because it's not covered by any automated tests.
-
On-resolve plugins now apply to entry points (#546)
Previously entry points were required to already be resolved to valid file system paths. This meant that on-resolve plugins didn't run, which breaks certain workflows. Now entry point paths are resolved using normal import resolution rules.
To avoid making this a breaking change, there is now special behavior for entry point path resolution. If the entry point path exists relative to the current working directory and the path does not start with
./or../, esbuild will now automatically insert a leading./at the start of the path to prevent the path from being interpreted as anode_modulespackage path. This is only done if the file actually exists to avoid introducing./for paths with special plugin-specific syntax. -
Enable the build API in the browser (#527)
Previously you could only use the transform API in the browser, not the build API. You can now use the build API in the browser too. There is currently no in-browser file system so the build API will not do anything by default. Using this API requires you to use plugins to provide your own file system. Instructions for running esbuild in the browser can be found here: https://esbuild.github.io/api/#running-in-the-browser.
-
Set the importer to
sourcefilein on-resolve plugins for stdinWhen the stdin feature is used with on-resolve plugins, the importer for any import paths in stdin is currently always set to
<stdin>. Thesourcefileoption provides a way to set the file name of stdin but it wasn't carried through to on-resolve plugins due to an oversight. This release changes this behavior so nowsourcefileis used instead of<stdin>if present. In addition, if the stdin resolve directory is also specified the importer will be placed in thefilenamespace similar to a normal file.
-
Fix an edge case with class body initialization
When bundling, top-level class statements are rewritten to variable declarations initialized to a class expression. This avoids a severe performance pitfall in Safari when there are a large number of class statements. However, this transformation was done incorrectly if a class contained a static field that references the class name in its own initializer:
class Foo { static foo = new Foo }In that specific case, the transformed code could crash when run because the class name is not yet initialized when the static field initializer is run. Only JavaScript code was affected. TypeScript code was not affected. This release fixes this bug.
-
Remove more types of statements as dead code (#580)
This change improves dead-code elimination in the case where unused statements follow an unconditional jump, such as a
return:if (true) return if (something) thisIsDeadCode()These unused statements are removed in more cases than in the previous release. Some statements may still be kept that contain hoisted symbols (
varandfunctionstatements) because they could potentially impact the code before the conditional jump.
-
Handle non-ambiguous multi-path re-exports (#568)
Wildcard re-exports using the
export * from 'path'syntax can potentially result in name collisions that cause an export name to be ambiguous. For example, the following code would result in an ambiguous export if botha.jsandb.jsexport a symbol with the same name:export * from './a.js' export * from './b.js'Ambiguous exports have two consequences. First, any ambiguous names are silently excluded from the set of exported names. If you use an
import * aswildcard import, the excluded names will not be present. Second, attempting to explicitly import an ambiguous name using animport {} fromimport clause will result in a module instantiation error.This release fixes a bug where esbuild could in certain cases consider a name ambiguous when it actually isn't. Specifically this happens with longer chains of mixed wildcard and named re-exports. Here is one such case:
// entry.js import {x, y} from './not-ambiguous.js' console.log(x, y)// /not-ambiguous.js export * from './a.js' export * from './b.js'// /a.js export * from './c.js'// /b.js export {x} from './c.js'// /c.js export let x = 1, y = 2Previously bundling
entry.jswith esbuild would incorrectly generate an error about an ambiguousxexport. Now this case builds successfully without an error. -
Omit warnings about non-string paths in
await import()inside atryblock (#574)Bundling code that uses
require()orimport()with a non-string path currently generates a warning, because the target of that import will not be included in the bundle. This is helpful to warn about because other bundlers handle this case differently (e.g. Webpack bundles the entire directory tree and emulates a file system lookup) so existing code may expect the target of the import to be bundled.You can avoid the warning with esbuild by surrounding the call to
require()with atryblock. The thinking is that if there is a surroundingtryblock, presumably the code is expecting therequire()call to possibly fail and is prepared to handle the error. However, there is currently no way to avoid the warning forimport()expressions. This release introduces an analogous behavior forimport()expressions. You can now avoid the warning with esbuild if you useawait import()and surround it with atryblock.
-
Fix a bug with certain complex optional chains (#573)
The
?.optional chaining operator only runs the right side of the operator if the left side is undefined, otherwise it returns undefined. This operator can be applied to both property accesses and function calls, and these can be combined into long chains of operators. These expressions must be transformed to a chain of?:operators if the?.operator isn't supported in the configured target environment. However, esbuild had a bug where an optional call of an optional property with a further property access afterward didn't preserve the value ofthisfor the call. This bug has been fixed. -
Fix a renaming bug with external imports
There was a possibility of a cross-module name collision while bundling in a certain edge case. Specifically, when multiple files both contained an
importstatement to an external module and then both of those files were imported usingrequire. For example:// index.js console.log(require('./a.js'), require('./b.js'))// a.js export {exists} from 'fs'// b.js export {exists} from 'fs'In this case the files
a.jsandb.jsare converted to CommonJS format so they can be imported usingrequire:// a.js import {exists} from "fs"; var require_a = __commonJS((exports) => { __export(exports, { exists: () => exists }); }); // b.js import {exists} from "fs"; var require_b = __commonJS((exports) => { __export(exports, { exists: () => exists }); }); // index.js console.log(require_a(), require_b());However, the
existssymbol has been duplicated without being renamed. This is will result in a syntax error at run-time. The reason this happens is that the statements in the filesa.jsandb.jsare placed in a nested scope because they are inside the CommonJS closure. Theimportstatements were extracted outside the closure but the symbols they declared were incorrectly not added to the outer scope. This problem has been fixed, and this edge case should no longer result in name collisions.
-
Get esbuild working on the Apple M1 chip via Rosetta 2 (#564)
The Go compiler toolchain does not yet support the new Apple M1 chip. Go version 1.15 is currently in a feature freeze period so support will be added in the next version, Go 1.16, which will be released in February.
This release changes the install script to install the executable for macOS
x64on macOSarm64too. Doing this should still work because of the executable translation layer built into macOS. This change was contributed by @sod.
-
Improve TypeScript type definitions (#559)
The return value of the
buildAPI has some optional fields that are undefined unless certain arguments are present. That meant you had to use the!null assertion operator to avoid a type error if you have the TypeScriptstrictNullCheckssetting enabled in your project. This release adds additional type information so that if the relevant arguments are present, the TypeScript compiler can tell that these optional fields on the return value will never be undefined. This change was contributed by @lukeed. -
Omit a warning about
require.mainwhen targeting CommonJS (#560)A common pattern in code that's intended to be run in node is to check if
require.main === module. That will be true if the current file is being run from the command line but false if the current file is being run because some other code calledrequire()on it. Previously esbuild generated a warning about an unexpected use ofrequire. Now this warning is no longer generated forrequire.mainwhen the output format iscjs. -
Warn about defining
process.env.NODE_ENVas an identifier (#466)The define feature can be used to replace an expression with either a JSON literal or an identifier. Forgetting to put quotes around a string turns it into an identifier, which is a common mistake. This release introduces a warning when you define
process.env.NODE_ENVas an identifier instead of a string. It's very common to use define to replaceprocess.env.NODE_ENVwith either"production"or"development"and sometimes people accidentally replace it withproductionordevelopmentinstead. This is worth warning about because otherwise there would be no indication that something is wrong until the code crashes when run. -
Allow starting a local server at a specific host address (#563)
By default, esbuild's local HTTP server is only available on the internal loopback address. This is deliberate behavior for security reasons, since the local network environment may not be trusted. However, it can be useful to run the server on a different address when developing with esbuild inside of a virtual machine/docker container or to request development assets from a remote testing device on the same network at a different IP address. With this release, you can now optionally specify the host in addition to the port:
esbuild --serve=192.168.0.1:8000esbuild.serve({ host: '192.168.0.1', port: 8000, }, { ... })server, err := api.Serve(api.ServeOptions{ Host: "192.168.0.1", Port: 8000, }, api.BuildOptions{ ... })This change was contributed by @jamalc.
-
Allow
pathswithoutbaseUrlintsconfig.jsonThis feature was recently released in TypeScript 4.1. The
pathsfeature intsconfig.jsonallows you to do custom import path rewriting. For example, you can map paths matching@namespace/*to the path./namespace/src/*relative to thetsconfig.jsonfile. Previously using thepathsfeature required you to additionally specifybaseUrlso that the compiler could know which directory the path aliases were supposed to be relative to.However, specifying
baseUrlhas the potentially-problematic side effect of causing all import paths to be looked up relative to thebaseUrldirectory, which could potentially cause package paths to accidentally be redirected to non-package files. SpecifyingbaseUrlalso causes Visual Studio Code's auto-import feature to generate paths relative to thebaseUrldirectory instead of relative to the directory containing the current file. There is more information about the problems this causes here: https://github.com/microsoft/TypeScript/issues/31869.With TypeScript 4.1, you can now omit
baseUrlwhen usingpaths. When you do this, it as if you had written"baseUrl": "."instead for the purpose of thepathsfeature, but thebaseUrlvalue is not actually set and does not affect path resolution. Thesetsconfig.jsonfiles are now supported by esbuild. -
Fix evaluation order issue with import cycles and CommonJS-style output formats (#542)
Previously entry points involved in an import cycle could cause evaluation order issues if the output format was
iifeorcjsinstead ofesm. This happened because this edge case was handled by treating the entry point file as a CommonJS file, which extracted the code into a CommonJS wrapper. Here's an example:Input files:
// index.js import { test } from './lib' export function fn() { return 42 } if (test() !== 42) throw 'failure'// lib.js import { fn } from './index' export let test = fnPrevious output (problematic):
// index.js var require_esbuild = __commonJS((exports) => { __export(exports, { fn: () => fn2 }); function fn2() { return 42; } if (test() !== 42) throw "failure"; }); // lib.js var index = __toModule(require_esbuild()); var test = index.fn; module.exports = require_esbuild();This approach changed the evaluation order because the CommonJS wrapper conflates both binding and evaluation. Binding and evaluation need to be separated to correctly handle this edge case. This edge case is now handled by inlining what would have been the contents of the CommonJS wrapper into the entry point location itself.
Current output (fixed):
// index.js __export(exports, { fn: () => fn }); // lib.js var test = fn; // index.js function fn() { return 42; } if (test() !== 42) throw "failure";
-
Fix a concurrency bug caused by an error message change (#556)
An improvement to the error message for path resolution was introduced in version 0.8.12. It detects when a relative path is being interpreted as a package path because you forgot to start the path with
./:> src/posts/index.js: error: Could not resolve "PostCreate" (use "./PostCreate" to import "src/posts/PostCreate.js") 2 │ import PostCreate from 'PostCreate'; ╵ ~~~~~~~~~~~~This is implemented by re-running path resolution for package path resolution failures as a relative path instead. Unfortunately, this second path resolution operation wasn't guarded by a mutex and could result in concurrency bugs. This issue only occurs when path resolution fails. It is fixed in this release.
-
Assigning to a
constsymbol is now an error when bundlingThis change was made because esbuild may need to change a
constsymbol into a non-constant symbol in certain situations. One situation is when the "avoid TDZ" option is enabled. Another situation is some potential upcoming changes to lazily-evaluate certain modules for code splitting purposes. Making this an error gives esbuild the freedom to do these code transformations without potentially causing problems where constants are mutated. This has already been a warning for a while so code that does this should already have been obvious. This warning was made an error in a patch release because the expectation is that no real code relies on this behavior outside of conformance tests. -
Fix for the
--keep-namesoption and anonymous lowered classesThis release fixes an issue where names were not preserved for anonymous classes that contained newer JavaScript syntax when targeting an older version of JavaScript. This was because that causes the class expression to be transformed into a sequence expression, which was then not recognized as a class expression. For example, the class did not have the name
fooin the code below when the target was set toes6:let foo = class { #privateMethod() {} }The
nameproperty of this class object is nowfoo. -
Fix captured class names when class name is re-assigned
This fixes a corner case with class lowering to better match the JavaScript specification. In JavaScript, the body of a class statement contains an implicit constant symbol with the same name as the symbol of the class statement itself. Lowering certain class features such as private methods means moving them outside the class body, in which case the contents of the private method are no longer within the scope of the constant symbol. This can lead to a behavior change if the class is later re-assigned:
class Foo { static test() { return this.#method() } static #method() { return Foo } } let old = Foo Foo = class Bar {} console.log(old.test() === old) // This should be truePreviously this would print
falsewhen transformed to ES6 by esbuild. This now printstrue. The current transformed output looks like this:var _method, method_fn; const Foo2 = class { static test() { return __privateMethod(this, _method, method_fn).call(this); } }; let Foo = Foo2; _method = new WeakSet(); method_fn = function() { return Foo2; }; _method.add(Foo); let old = Foo; Foo = class Bar { }; console.log(old.test() === old); -
The
--allow-tdzoption is now always applied during bundlingThis option turns top-level
let,const, andclassstatements intovarstatements to work around some severe performance issues in the JavaScript run-time environment in Safari. Previously you had to explicitly enable this option. Now this behavior will always happen, and there is no way to turn it off. This means the--allow-tdzoption is now meaningless and no longer does anything. It will be removed in a future release. -
When bundling and minifying,
constis now converted intoletThis was done because it's semantically equivalent but shorter. It's a valid transformation because assignment to a
constsymbol is now a compile-time error when bundling, so changingconsttoletshould now not affect run-time behavior.
-
Added an API for incremental builds (#21)
There is now an API for incremental builds. This is what using the API looks like from JavaScript:
require('esbuild').build({ entryPoints: ['app.js'], bundle: true, outfile: 'out.js', incremental: true, }).then(result => { // The "rebuild" method is present if "incremental" is true. It returns a // promise that resolves to the same kind of object that "build" returns. // You can call "rebuild" as many times as you like. result.rebuild().then(result2 => { // Call "dispose" when you're done to free up resources. result.rebuild.dispose() }) })Using the API from Go is similar, except there is no need to manually dispose of the rebuild callback:
result := api.Build(api.BuildOptions{ EntryPoints: []string{"app.js"}, Bundle: true, Outfile: "out.js", Incremental: true, }) result2 := result.Rebuild()Incremental builds are more efficient than regular builds because some data is cached and can be reused if the original files haven't changed since the last build. There are currently two forms of caching used by the incremental build API:
-
Files are stored in memory and are not re-read from the file system if the file metadata hasn't changed since the last build. This optimization only applies to file system paths. It does not apply to virtual modules created by plugins.
-
Parsed ASTs are stored in memory and re-parsing the AST is avoided if the file contents haven't changed since the last build. This optimization applies to virtual modules created by plugins in addition to file system modules, as long as the virtual module path remains the same.
This is just the initial release of the incremental build API. Incremental build times still have room for improvement. Right now esbuild still re-resolves, re-loads, and re-links everything even if none of the input files have changed. Improvements to the incremental build mechanism will be coming in later releases.
-
-
Support for a local file server (#537)
You can now run esbuild with the
--serveflag to start a local server that serves the output files over HTTP. This is intended to be used during development. You can point your<script>tag to a local server URL and your JavaScript and CSS files will be automatically built by esbuild whenever that URL is accessed. The server defaults to port 8000 but you can customize the port with--serve=....There is also an equivalent API for JavaScript:
require('esbuild').serve({ port: 8000, },{ entryPoints: ['app.js'], bundle: true, outfile: 'out.js', incremental: true, }).then(server => { // Call "stop" on the server when you're done server.stop() })and for Go:
server, err := api.Serve(api.ServeOptions{ Port: 8000, }, api.BuildOptions{ EntryPoints: []string{"app.js"}, Bundle: true, Outfile: "out.js", Incremental: true, }) // Call "stop" on the server when you're done server.Stop()This is a similar use case to "watch mode" in other tools where something automatically rebuilds your code when a file has changed on disk. The difference is that you don't encounter the problem where you make an edit, switch to your browser, and reload only to load the old files because the rebuild hasn't finished yet. Using a HTTP request instead of a file system access gives the rebuild tool the ability to delay the load until the rebuild operation has finished so your build is always up to date.
-
Install to a temporary directory for Windows (#547)
The install script runs
npmin a temporary directory to download the correct binary executable for the current architecture. It then removes the temporary directory after the installation. However, removing a directory is sometimes impossible on Windows. To work around this problem, the install script now installs to the system's temporary directory instead of a directory inside the project itself. That way it's not problematic if a directory is left behind by the install script. This change was contributed by @Djaler. -
Fix the public path ending up in the metafile (#549)
The change in version 0.8.7 to include the public path in import paths of code splitting chunks caused a regression where the public path was also included in the list of chunk imports in the metafile. This was unintentional. Now the public path setting should not affect the metafile contents.
-
Fix parsing of casts in TypeScript followed by certain tokens
This aligns esbuild's TypeScript parser with the official TypeScript parser as far as parsing of
ascasts. It's not valid to form an expression after anascast if the next token is a(,[,++,--,?., assignment operator, or template literal. Previously esbuild wouldn't generate an error for these expressions. This is normally not a problem because the TypeScript compiler itself would reject the code as invalid. However, if the next token starts on a new line, that new token may be the start of another statement. In that case the code generated by esbuild was different than the code generated by the TypeScript compiler. This difference has been fixed. -
Implement wildcards for external paths (#406)
You can now use a
*wildcard character with the--externaloption to mark all files matching a certain pattern as external, which will remove them from the bundle. For example, you can now do--external:*.pngto remove all.pngfiles. When a*wildcard character is present in an external path, that pattern will be applied to the original path in the source code instead of to the path after it has been resolved to a real file system path. This lets you match on paths that aren't real file system paths. -
Add a warning about self-assignment
This release adds a warning for code that assigns an identifier to itself (e.g.
x = x). This code is likely a mistake since doing this has no effect. This warning is not generated for assignments to global variables, since that can have side effects, and self-assignments with TypeScript casts, since those can be useful for changing the type of a variable in TypeScript. The warning is also not generated for code inside anode_modulesfolder.
-
Fix parsing of conditional types in TypeScript (#541)
Conditional types in TypeScript take the form
A extends B ? C : D. Parsing of conditional types in esbuild was incorrect. The?can only follow anextendsclause but esbuild didn't require theextendsclause, which potentially led to build failures or miscompilation. The parsing for this syntax has been fixed and should now match the behavior of the TypeScript compiler. This fix was contributed by @rtsao. -
Ignore comments for character frequency analysis (#543)
Character frequency analysis is used to derive the order of minified names for better gzip compression. The idea is to prefer using the most-used characters in the non-symbol parts of the document (keywords, strings, etc.) over characters that are less-used or absent. This is a very slight win, and is only approximate based on the input text instead of the output text because otherwise it would require minifying twice.
Right now comments are included in this character frequency histogram. This is not a correctness issue but it does mean that documents with the same code but different comments may be minified to different output files. This release fixes this difference by removing comments from the character frequency histogram.
-
Add an option to ignore tree-shaking annotations (#458)
Tree shaking is the term the JavaScript community uses for dead code elimination, a common compiler optimization that automatically removes unreachable code. Since JavaScript is a dynamic language, identifying unused code is sometimes very difficult for a compiler, so the community has developed certain annotations to help tell compilers what code should be considered unused. Currently there two forms of tree-shaking annotations that esbuild supports: inline
/* @__PURE__ */comments before function calls and thesideEffectsfield inpackage.json.These annotations can be problematic because the compiler depends completely on developers for accuracy and the annotations are occasionally incorrect. The
sideEffectsfield is particularly error-prone because by default it causes all files in your package to be considered dead code if no imports are used. If you add a new file containing side effects and forget to update that field, your package will break when people try to bundle it.This release adds a new flag
--tree-shaking=ignore-annotationsto allow you to bundle code that contains incorrect tree-shaking annotations with esbuild. An example of such code is @tensorflow/tfjs. Ideally the--tree-shaking=ignore-annotationsflag is only a temporary workaround. You should report these issues to the maintainer of the package to get them fixed since they will trip up other people too. -
Add support for absolute
baseUrlpaths intsconfig.jsonfilesPreviously esbuild always joined the
baseUrlpath to the end of the current directory path. However, if thebaseUrlwas an absolute path, that would end up including the current directory path twice. This situation could arise internally in certain cases involving multipletsconfig.jsonfiles andextendsfields even if thetsconfig.jsonfiles themselves didn't have absolute paths. Absolute paths are now not modified and should work correctly. -
Fix crash for modules that do
module.exports = null(#532)The code generated by esbuild would crash at run-time if a module overwrote
module.exportswith null or undefined. This has been fixed and no longer crashes.
-
Add support for the
mips64learchitecture (#523)You should now be able to install esbuild on the
mips64learchitecture. This build target is second-tier as it's not covered by CI, but I tested it in an emulator and it appears to work at the moment. -
Fix for packages with inconsistent side effect markings
Packages can have multiple entry points in their
package.jsonfile. Two commonly-used ones are specified using the fieldsmainandmodule. Packages can also mark files in the package as not having side effects using thesideEffectsfield. Some packages have one entry point marked as having side effects and the other entry point as not having side effects. This is arguably a problem with the package itself. However, this caused an issue with esbuild's automatic entry point field selection method where it would incorrectly consider bothmainandmoduleto not have side effects if one of them was marked as not having side effects. Nowmainandmodulewill only be considered to not have side effects if the individual file was marked as not having side effects. -
Warn about
import './file'when./filewas marked as having no side effectsFiles in packages containing
"sideEffects": falsein the enclosingpackage.jsonfile are intended to be automatically removed from the bundle if they aren't used. However, code containingimport './file'is likely trying to import that file for a side effect. This is a conflict of intentions so it seems like a good idea to warn about this. It's likely a configuration error by the author of the package. The warning points to the location inpackage.jsonthat caused this situation. -
Add support for glob-style tests in
sideEffectsarraysThe
sideEffectsfield inpackage.jsoncan optionally contain an array of files that are considered to have side effects. Any file not in that list will be removed if the import isn't used. Webpack supports the*and?wildcard characters in these file strings. With this release, esbuild supports these wildcard characters too.