Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This is a PR for version 0.11.0, which is a release that contains the following breaking changes:
Change how
require()
andimport()
of ESM works (fixes Circular dependency issue #667, fixes ESM entry module's exports signature is not preserved whenrequired()
by other modules #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:The previous bundling behavior would generate something like this:
This behavior has been changed and esbuild now generates something like this instead:
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 (ESM entry module's exports signature is not preserved when
required()
by other modules #706) where entry point exports could be broken if the entry point is a target of arequire()
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 (support for 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 ([Feature] Manual chunks #207), correct import evaluation order (Incorrect import order with code splitting and multiple entry points #399), and correct top-level await evaluation order (support for top level await #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 (Circular dependency issue #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 (fixes --entry-names=[name] errors out for some dynamic imports #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
JS
Go
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 (fixes onResolve shouldn't change output path for entry points #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 (fixes onResolve is not fired on entrypoints when namespace equals 'file' #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.