v0.11.0
This release contains backwards-incompatible changes. Since esbuild is before version 1.0.0, these changes have been released as a new minor version to reflect this (as recommended by npm). You should either be pinning the exact version of esbuild
in your package.json
file or be using a version range syntax that only accepts patch upgrades such as ~0.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 referencefoo
directly 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 singledefault
export, 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 beasync
and 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 animport
statement 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 defaultoutbase
is 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
outbase
directory to the entry point path, and then joining that relative path to theoutdir
directory. 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.js
andout/out2.js
inside 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=out
would generate the output pathout/index.js
because the input pathreact
was 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=out
now 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
file
namespace for file entry points (#791)Plugins that contain an
onResolve
callback with thefile
filter 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.js
with 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
file
for 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
WeakSet
to 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.